前言

Java中的主流虚拟机HotSpot采用可达性分析算法来确定一个对象的状态,那么HotSpot是如何实现该算法的呢?

使用OopMap记录并枚举根节点

在从GC ROOT向下查找引用链时,可作为GC ROOT的节点主要有全局性引用(常量、静态变量)和执行上下文(栈帧中的本地变量表),通常方法区就有好几百兆,遍历一次需要比较长的时间。

另外,在查找引用链过程中,需要保证引用链的一致性,即在分析过程中对象的引用关系不再变化,否则分析准确性则无法得到保证。因此通常GC执行时会stop the world,停止所有执行线程,即使几乎不发生停顿的CMS收集器中,枚举根节点也是需要停顿的。如果这个停顿的时间过长,肯定是难以接受的。

那么HotSpot是如何在最短的时间内进行这个耗时的搜索工作的呢?

答案就在这个神奇的数据结构中——OopMap。

在遍历堆栈时,虚拟机关心的就是“这块数据是不是指针”。为了让JVM能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。HotSpot选择从外部记录下类型信息,存成映射表,也就是OopMap。

OopMap在类加载完成时把对象内的偏移量是什么类型计算出,并且存放下在相应的位置,当需要遍历根结点时访问所有OopMap即可。在JIT编译过程中,也会在特定位置记录下栈和寄存器中的那些位置和引用的,这样GC在扫描时就可以直接获得这些信息。

用安全点Safepoint约束根节点

如果将每个符合GC Roots条件的对象都存放进入OopMap中,那么OopMap也会变得很大,而且其中很多对象很可能会发生一些变化,这些变化使得维护这个映射表很困难。

实际上,HotSpot并没有为每一个对象都创建OopMap,只在特定的位置上创建了这些信息,这些位置称为安全点(Safepoints)。

为了保证虚拟机中安全点的个数不算太多也不是太少,主要决定安全点是否被建立的因素是时间。当进行了耗时的操作时,比如方法调用、循环跳转等时会产生安全点。

在GC发生时需要让线程停顿下来,让线程停顿下来的方案有两种,抢先式中断主动式中断

  • 抢先式中断:在GC发生时先中断所有线程,如果线程不在安全点上,则启动该线程使其执行到安全点后挂起。

  • 主动式中断:不需要直接对线程进行操作,在线程执行时主动轮询这个标识,若中断标识为真,在线程自己中断挂起这个标识和安全点是重合的

安全区域(safe region)

上面的安全点检查仿佛完全解决了如何进入GC的问题,但只有安全点还是不够的,安全点只解决了那些在运行的程序,保证了他们可以运行到安全点并挂起,但如果有些线程此时并未执行,例如处于sleep或blocked状态的线程,就无法响应JVM的中断请求,这时就需要使用安全区域了。

安全区域是指在此区域内,对象的引用关系不会发生变化(即不会影响枚举根节点)

当线程运行到安全区域时会将自己标识,在JVM准备进行GC时将视这些线程为安全的,不影响GC,当线程运行完毕要离开安全区域时,线程会检查JVM是否在枚举根节点,若是,则等待完成后再离开安全区域继续执行。