二、JVM垃圾收集机制

1、对象的生死

             可达性分析算法
      可达性分析算法用于判定对象是否是存活的,其基本思想是,通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在Java语言中,可作为GC Roots的对象包括下面几种:
      1.虚拟机栈(栈帧中的本地变量表)中引用的对象;
      2.方法区中类静态属性引用的对象;
      3.方法区中常量引用的对象;
      4.本地方法栈中JNI(既一般说的Native方法)引用的对象。
      即使在可达性分析算法中不可达的对象,也并非是非死不可的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程;如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
      如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没逃脱,那么基本上它就真的被回收了。

2、标记-清除算法(Mark-Sweep)

      标记-清除算法分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。它主要有两个不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3、标记-复制算法(Mark-Copy)

      复制算法为了解决效率问题,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只是这种算法的代价是将内存缩小为了原来的一半,代价太高。
      商业虚拟机都采用这种算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor空间。当回收时,将EdenSurvivor中还存活着的对象一次性地复制到另一块Survivor上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认EdenSurvivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存被浪费。当然,98%的对象可回收只是一半场景下的数据,我们没办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖老年代进行分配担保(Handle Promotion),如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

4、标记-整理算法(Mark-Compact)

      标记-整理算法的标记过程和“标记-清除”算法一样,但是后续的整理过程是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

5、分代收集算法(Generational Collection)

       分代收集算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆分为新生代和老年代,新生代对象存活率较低,采用复制算法,而老年代对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。

6、HotSpot的算法实现——枚举根节点

      可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。
      可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能够确保一致性的快照中进行,这里的“一致性”的意思是指在整个分析期间整个执行系统看起来就像冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这一点是导致GC进行时必须停顿所有Java执行线程(Sun将这件事称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
      在目前的主流Java虚拟机中,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知那些地方存放着对象引用。在HotSpot的实现中,是使用组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中那些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。

7、HotSpot的算法实现——安全点

      在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
      实际上,HotSpot也的确没有为每一条指令都生成OopMap,只是在特定的位置记录了这些信息,这些位置称为安全点(Safe Point),既程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safe Point的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safe Point
      对于Safe Point,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都跑到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断和主动式中断,其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而影响GC事件;主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

8、HotSpot的算法实现——安全区域

      使用Safe Point似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定。Safe Point机制保证了程序执行时,在不太长的时间就会遇到可进入GC的Safe Point。但是,程序不执行的时候呢,所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,走到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,我们也可以把Safe Region看做是被扩展了的Safe Point。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则他就必须等待直到收到可以安全离开Safe Region的信号为止。

参考资料
1.《深入理解Java虚拟机:JVM高级特性与最佳实践》(第2版)。

posted @ 2019-06-22 23:38  萧亦全  阅读(178)  评论(0)    收藏  举报