JVM介绍

  • JVM为Java程序的运行平台,屏蔽了操作系统的差异,为Java程序提供统一的运行环境,可以让Java程序做到一次编写,到处运行。

JVM体系结构

  • Java 虚拟机的主要组件,包括类加载器运行时数据区执行引擎诊断系统

JVM内存布局

  • 整个JVM中,将内存划分为程序计数器,堆,栈,元空间(方法区)
  • 程序计数器,一个线程私有的内存结构,负责保存线程运行时所执行的字节码地址(如果执行的是native方法,则不会保存任何数据,被标记为undefined),程序运行中出现的分支判断,流程控制,都是依赖它实现的,每当Java解释器在运行过程中,通过改变这个计数器,实现程序的运行。
  • 栈,其实又被细分为虚拟机栈和本地方法栈,栈中内存,被用于保存线程执行的方法时,方法的局部变量表,操作数栈,程序入参和出参信息等,在hotspot虚拟机中,将本地方法栈合并到虚拟机栈中统一实现。
    • 本地方法栈,虚拟机规范规定此部分的内存,只供native方法使用。
    • 虚拟机栈,Java程序运行时使用的栈,供非native方法使用。
  • 元空间实际上是Java虚拟机规范中规定的方法区的具体实现,在虚拟机规范中,方法区被声明为一个逻辑区域,其具体实现因虚拟机而异,比如,早期的hotspot虚拟机,在使用分代设计思想实现堆栈的时候,扩展了分代设计,使用所谓的永久代来作为方法区的具体实现,而Java8以后的版本,使用直接内存来实现了实现的元空间作为方法区的实现,在方法区中,实际保存的东西有,常量,静态变量,所加载的类的类型信息,运行时常量池,以及及时编译的代码缓存。

Java内存模型

  • JMM规定,线程操作某个变量,必须从共享内存中拷贝到自己的工作内存中,并在自己工作线程中修改,再存放回共享内存中,若修改过程变量再放回内存的过程中,该变量被其他线程所改变,则当前变量值作废,必须重新从内存中获取新的变量值进行操作。

  • JMM定义了8种操作,保证主内存和工作内存中的变量一致性,且每一个操作,都要是原子性的操作。

    • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用

    • load(载入):作用于工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

    • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

    • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。

    • write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

  • 其中,read与load,必须按顺序执行,store和write也是必须按照顺序执行,但是可以不连续,比如read1,read2,load1,load2,上面8个操作,还对应了8个规则,当执行8个操作中的某一操作时,也就要满足对应的规则

    • 1 不允许 read 和 load、store 和write 操作之一单独出现

    • 2 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。

    • 3 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。

    • 4 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。

    • 5 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现

    • 6 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值

    • 7 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量

    • 8 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作

  • 通过插入内存屏障,禁止指令重排,遵循上述8种规则,保证有序性和可见性,通过lock和unlock规则,保证了原子性。

  • Java中的内存屏障

    Loadloadload1
    loadload
    load2
    确保load1在load2或其他load操作之前执行
    StoreStorestore1
    storestore
    store2
    确保Store1数据对其他处理器可见(刷新到内存),先于Store2及所有后续存储指令的存储。
    LoadStoreload1
    loadStore
    store2
    确保Load1数据装载先于Store2及所有后续存储指令的存储。
    StoreLoadstore1
    storeLoad
    load2
    确保Store1数据对其他处理器可见(刷新到内存)先于Load2及所有后续装载指令的装载。

    GC

    • 垃圾收集器,负责回收Java堆内存中的无用对象,管理内存空间。
    • 在HotSpot VM中,目前存在的垃圾收集器有serial,serial Old, CMS,G1,Parallel scavenge, parallel old,parnew。
    • 垃圾收集三大基础算法,标记-复制,标记-清除,标记-整理
    • 以Java8为代表,在8之前,堆内存都是基于分代理论进行设计,因此,上述的收集器也只是在对应的分代中使用,Java8默认使用parallel收集器组合来进行垃圾回收,新生代使用parNew, 老年代使用的parallel Old, 对Java的垃圾收集器选择,根据延迟和吞吐量这两个维度的侧重点不同,具体的收集器选型能做出适当调整,parallel系列,以吞吐量为主,而cms以延迟为主,新款收集器g1,区别于传统的内存划分方式,以region为单位,被誉为cms的接班人,Java9的默认收集器。

    新生代

    • Eden, s0, s1,默认8:1:1,发生一次young GC,eden中没被回收掉的对象,会进入s0,第二次会进入s1,如此循环,如果一次回收后,存活对象在s区放不下,老年代满足内存担保,则进入老年代。

    老年代

    • 因为老年代有内存分配担保机制,新创建的大对象会直接进入老年代,如果老年代无法满足内存分配担保机制,会进行full GC,回收后仍无法满足,则会抛出OOM。

    调优参数

    • xms, xmx -- 堆内存最小/最大值,一般来说,整个堆内存,应该为物理内存的一半左右较为合适,如果服务器内除了Java应用还有其他应用,需要另外考虑

    • Xss,线程栈最大容量

    • gclog,打印gc日志,方便问题排查。

    • Xmn, 调节新生代大小,但是新生代大小再怎么大,也不会超过整堆,新生代+老年代=整堆,常规的gc调优思路,就是尽可能让full gc的收集次数少,越少越好,如果每次young gc都能带走大部分小对象,留出大部分可用内存,证明程序写的可以。

    • PretenureSizeThreshold,设置直接进入老年代的对象阈值,毕竟,在新生代回收大对象简直就是噩梦,除此之外,编写程序时也应该考虑,如果期间会有大对象的创建,是否可以让对象被多次使用,或者把大对象拆小。

    • MaxTenuringThreshold,设置对象晋升阈值,最大15,合理设置可以减少老年代空间的占用,降低full gc发生的频率

    • 如果gc的情况类似如下的指标,则可以不考虑优化。

      MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。

Q.E.D.