GC安全点

#OopMap

在正式的 GC 之前,要进行可达性分析来标记出将来可能要宣告死亡的对象。如果每次 GC 的时候都要遍历所有的引用,这样的工作量是非常大的。因为在可达性分析的时候要保证期间不发生引用关系的变化,所有执行线程要停顿等待,称为“Stop The World”,程序中的线程需要停止来配合可达性分析。

所以,每次直接遍历整个引用链肯定是不现实的。 为了应对这种尴尬的问题,最早有保守式 GC 和后来的准确式 GC 。这里准确式GC就会提到一个 OopMap,用来保存类型的映射表。

#保守式 GC

在进行 GC 的时候,会从一些已知的位置(GC Roots)开始扫描内存,扫描到一个数字就判断他是不是可能是指向 GC 堆中的一个指针【这里会涉及上下边界检查( GC 堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针)等】,然后一直递归扫描下去,最后完成可达性分析。这种模糊的判断方法因为无法准确判断一个位置上是否是真的指向 GC 堆中的指针,所以被命名为保守式 GC

优点:这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快。

缺点:

  • 因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用它们,GC 也就自然不会回收它们,从而引起了无用的内存占用,造成资源浪费。
  • 由于不知道疑似指针是否真的是指针,所以它们的值都不能改写,移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式 GC 的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK 的 Classic VM 用过这种全 handle 的设计,但效果实在算不上好。
#准确式 GC

与保守式 GC 相对的就是准确式 GC 。何为准确式 GC ?就是我们准确的知道,某个位置上面是否是指针。对于 java 来说,就是知道对于某个位置上的数据是什么类型的,这样就可以判断出所有的位置上的数据是不是指向 GC 堆的引用,包括栈和寄存器里的数据。

实现这种要求的方法有好几种,但是在 java 中实现的方式是:从外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为 OopMap ,不同的虚拟机名称可能不一样。

实现这种功能,需要虚拟机的解释器和 JIT 编译器支持,由它们来生成 OopMap。生成这样的映射表一般有两种方式:

  • 每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
  • 为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。

总而言之,GC 停顿的时候,虚拟机可以通过 OopMap 这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录着栈和寄存器中哪些位置是引用。

#Safe Point

有了 OopMap,HotSpot 可以快速准确完成 GC Roots 枚举。但是另一个问题来了,我们要在什么地方创建 OopMap ?程序运行期间,引用的变化在不断发生,若每条指令都生成 OopMap,那占用空间就太大了,所以有了**安全点(Safe Point)。只在安全点进行 GC 停顿,只要保证引用变化的记录完成于 GC 停顿之前就可以。

安全点选定太少,GC 等待时间就太长,选的太多,GC 就过于频繁。选定原则是“具有让程序长时间执行的特征”,也就是在这个时刻现有的指令是可以复用的。一般选在方法调用、循环跳转、抛出异常的位置。

如何使线程在 Safe Point 停顿,方案有两种:抢先式中断(弃用)、主动式中断

  • 抢先式中断:GC 发生时,中断所有线程,如果发现有线程不在安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。
  • 主动式中断:设置一个标志,和安全点重合。各个线程主动轮询这个标志,发现中断标志位就将线程中断挂起。HotSpot使用主动式中断,避免了抢占式中断的“中断-启动-中断”过程。

#Safe Region

安全点可以保证大部分线程停顿,但是当 GC 请求中断时线程并没有获取 CPU 执行权,线程无法响应 JVM “跑到”安全点,但是JVM的GC不会等待线程获得 CPU 执行权再对它进行可达性分析或者回收。也就是说该线程现在在非安全点停止,并且GC对其进行了操作,这样的操作是不满足一致性的,当线程苏醒之后就会发生下面的情况,即线程在继续执行,GC 也在操作该线程(这个过程对象的引用关系有可能会改变)。

安全区域(Safe Region)就可以很好地解决这样的问题:安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生 GC 中断请求都是安全的。当线程执行到安全区域时,首先标识自己已经进入了安全区域。在线程离开安全区域时,会检查系统是否正在执行 GC,如果是,就等到 GC 完成后再离开安全区域。