在正式的GC之前,要进行可达性分析来标记出将来可能要宣告死亡的对象。如果每次GC的时候都要遍历所有的引用,这样的工作量是非常大的。因为在可达性分析的时候要保证期间不发生引用关系的变化,所有执行线程要停顿等待,称为“Stop The World”,程序中的线程需要停止来配合可达性分析。
所以,每次直接遍历整个引用链肯定是不现实的。 为了应对这种尴尬的问题,最早有保守式GC和后来的准确式GC。这里准确式GC就会提到一个OopMap,用来保存类型的映射表。
保守式GC
在进行GC的时候,会从一些已知的位置(GC Roots)开始扫描内存,扫描到一个数字就判断他是不是可能是指向GC堆中的一个指针(这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。)。然后一直递归的扫描下去,最后完成可达性分析。这种模糊的判断方法因为无法准确判断一个位置上是否是真的指向GC堆中的指针,所以被命名为保守式GC。这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快,但是也正因为这种特点,他存在下面两个明显的缺点:
1.因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用他们,GC也就自然不会回收他们,从而引起了无用的内存占用,造成资源浪费。
2.由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。
准确式GC
与保守式GC相对的就是准确式GC,何为准确式GC?就是我们准确的知道,某个位置上面是否是指针,对于java来说,就是知道对于某个位置上的数据是什么类型的,这样就可以判断出所有的位置上的数据是不是指向GC堆的引用,包括栈和寄存器里的数据。
网上看了下说是实现这种要求的方法有好几种,但是在java中实现的方式是:从我外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为OopMap,不同的虚拟机名称可能不一样。
实现这种功能,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap。生成这样的映射表一般有两种方式:
总而言之,GC停顿的时候,虚拟机可以通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录着栈和寄存器中哪些位置是引用。
有了OopMap,HotSpot可以快速准确完成GC Roots枚举。但是另一个问题来了,我们要在什么地方创建OopMap?程序运行期间,引用的变化在不断发生,如果每一条指令都声称OopMap,那占用空间就太大了,所以有了安全点(Safe Point)。只在安全点进行GC停顿,只要保证引用变化的记录完成于GC停顿之前就可以。
安全点选定太少,GC等待时间就太长,选的太多,GC就过于频繁。选定原则是”具有让程序长时间执行的特征“,也就是在这个时刻现有的指令是可以复用的。一般选在方法调用、循环跳转、抛出异常的位置。
现在的问题是在Safe Point让线程们以怎样的机制中断,方案有两种:抢先式中断、主动式中断。
抢先式中断:GC发生时,中断所有线程,如果发现有线程不再安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。
主动式中断:设置一个标志,和安全点重合,再加上创建对象分配内存的地方。各个线程主动轮询这个标志,发现中断标志为真就挂起自己。HotSpot使用主动式中断。
1.OopMap
之前我们提到,在正式的GC之前总是需要进行可达性分析来查找内存中所有存活的对象,以便GC能够正确的回收已经死亡的对象。那么对于一个十分复杂的系统,每次GC的时候都要遍历所有的引用肯定是不现实的。因为在可达性分析的时候,需要进行Stop The World,程序中的线程需要停止来配合可达性分析。就好像是你女朋友在打扫卫生的时候(什么,你还没有女朋友?这还能难道程序员了?new 一个啊!),肯定不会让你走来走去的。所以,你肯定在内心里也希望你女朋友打扫卫生快一点,因为你的膀胱已经快要爆炸了。对于程序来说有也一样,也希望GC的时候快一点,以便让程序高效地完成工作。
所以,每次直接遍历整个引用链肯定是不现实的。 为了应对这种尴尬的问题,最早有保守式GC和后来的准确式GC。这里准确式GC就会提到一个OopMap,用来保存类型的映射表。
Safepoint保证了程序执行时,在不长的时间里就会遇到可进入GC的安全点,但如果线程没有分配cpu时间,必须线程处于sleep或blocked状态,就无法响应JVM的中断请求,走到安全点去挂起。Safe Region解决了这一问题。
安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生GC都是安全的。当代码执行到安全区域时,首先标识自己已经进入了安全区域,那样如果在这段时间里JVM发起GC,就不用管标示自己在安全区域的那些线程了,在线程离开安全区域时,会检查系统是否正在执行GC,如果是,就等到GC完成后再离开安全区域。