一、垃圾收集算法
1.1、标记-清除
该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象,标记过程在前一章节中已经介绍过。它是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进。它的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高,另一个是空间问题,标记清除后会产生大量连续的内存碎片,空间碎片太多可能会导致以后在运行过程中当需要为大对象分配内存时,无法找到足够的连续空间而不得不提前触发另一次垃圾收集动作。
1.2、复制算法
这种算法将可用内存按容量大小划分为大小相等的两块,每次只使用其中的一块。当一块的内存使用完了,就将这一块内存中还存活的对象复制到另一块中,再把它的内存空间一次清理掉。这样每次都是对半块区域进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。但是这种算法的代价是每次只能使用可用内存的一半。
现在的虚拟机都采用这种算法来回收新生代,并且由于新生代中98%的对象都是“朝生夕灭”的,所以并不要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior。当回收时,将Eden和Survior中存活的对象一次性复制到另一块Survior中,然后清除掉Eden和Survior中用过的空间。然后在继续使用Eden和有存活对象的一块Survior内存。
HotSpot虚拟机默认Eden和Survior的大小比例时8:1。这样就只有10%的内存会被“浪费”,但是我们没有办法未使用的Survior内存大小在垃圾回收时一定能够用。当Survior空间不够时,需要依赖老年代进行分配担保。
1.3、标记-整理
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中对象存都100%存活的极端情况。因为老年代就是存活率较高的一块内存。
所以根据老年代的特点,使用“标记-整理算法”,标记过程仍然与“标记-清除”算法一样,但是后续步骤不是直接对可回收内存进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。
1.4、分代收集算法
现在的java虚拟机会根据对象存活周期的不同将内存分为几块。一般是把堆分为新生代和老年代,这样就可以根据各自的特点采用最适当的收集算法。新生代中的对象,在每次垃圾收集时都发现有大批对象死去,所以就选用复制算法,只需要付出少量存活对象的复制成本就可以完成 收集。在老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来实现。
二、HotSpot的算法实现
上面介绍了对象存活判断算法和垃圾收集算法,在虚拟机上实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。
2.1、枚举根节点
GC Roots的节点主要在全局的引用(例如常量和类静态变量)与执行上下文(例如栈帧中的本地变量表)中,现在的很多应用仅仅元空间就有数百兆,如果要逐个检查里面的引用,那么必然会消耗很多时间。
另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析必须在一个能确保一致性的快照中进行——这里的“一致性”是指整个分析期间整个执行系统看起来就像被冻结在某个时间点上。这点是导致GC进行时必须停顿所有java执行线程(“Stop The World”)的其中一个重要原因,即使在机会不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
现在的虚拟机都使用的准确式GC,所以当执行系统停顿下来后,不需要一个个地检查多有执行上下文和全局的引用位置,在HotSpot的实现中,使用的是一组称为OopMap的数据结构来达到这个目的的,在类加载完的时候,虚拟机就把对象内什么偏移量式什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置式引用。这样。GC在扫描时就可以直接得知这些信息了。
2.2、安全点
借助OopMap的帮助,HotSpot可以快速准确地完成GC Roots枚举。但是HotSpot不是为每条指定都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点,即程序并非在所有地方都能停顿下来开始GC,只有在安全点时才能暂停。安全点不能太多也不能太少,所以安全点的选定基本上时以程序“是否具有让程序长时间执行的特征”为标准进行选定的。一般在具有方法调用、循环跳转、异常跳转等这些功能的指令才会产生安全点。
对于安全点,另外一个需要考虑的问题是如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来。一般有两种方案:抢先式中断和主动式中断,其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程中断,如果发现有线程中断的地方不是在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程而相应GC事件。
主动式的思想就是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
2.3、安全区域
安全点解决了如何进入GC的问题,但是如果程序“不执行”的时候呢?就是当线程没有分配CPU时间,典型的例子就是线程处于Sleep和Blocked状态。这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间,对于这种情况就需要安全区域。
安全区域是指在一段代码中,引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的,我们也可以把安全区域看作是扩展了的安全点。
在线程执行到安全区域中的代码时,首先标记自己进入安全区域了,那样当这段时间JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。在线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续进行,否则它就必须等待直到收到可以安全离开安全区域的信号为止。