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