jvm

JVM 系列 垃圾对象判断依据

开启JVM探索新篇章

Posted by lichao modified on November 7, 2019

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1.

存在的问题

很难解决对象之间相互循环引用的问题。

可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

可作为 GC Roots 的节点包括 全局性的引用(常量或类静态属性)和执行上下文:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象

保守式GC

在进行 GC 时,会从 GC Roots 开始扫描,扫描到一个数据时,需判断是否可能是指向 Java 堆中的一个指针。然后一直递归的扫描下去,最后完成可达性分析。这种模糊的判断方法因为无法准确判断一个位置上数据是否真的是指向 Java 堆中的指针,所以被命名为保守式GC。这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快,但是也正因为这种特点,它存在下面两个明显的缺点:

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

准确式GC

主流 Java虚拟机使用的都是准确式GC

能够确定某个位置上是否是指针,就是知道对于某个位置上的数据是什么类型的,这样就可以判断出所有的位置上的数据是不是指向 Java堆 的引用,包括栈和寄存器里的数据。

实现这种功能,需要虚拟机的解释器和JIT编译器支持,由它们来生成OopMap。

OopMap

在 OopMap 的协助下,Hotspot 可以快速完成 GC Roots 枚举。包括:

  • 在特定的位置(安全点)记录下栈和寄存器中哪些位置是引用。使在栈内存中能够得知哪些地方存放着对象引用
  • 在类加载完成时,把对象内什么偏移量上是什么类型的数据计算出来

安全点

从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停暂停所以活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。在安全点上,利用 Oopmap 快速进行可达性分析。

  1. 抢占式中断(不采用):在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它运用到安全点。
  2. 主动式中断(采用):Jvm设置一个标志,当程序运行到安全点时就去轮训该标志,发现该标志被设置为真时就自己中断挂起。所以轮训标志的地方是和安全点重合的,另外创建对象需要分配内存的地方也需要轮询该位置。

一般会在如下几个位置选择安全点:

  1. 循环的末尾
  2. 方法临返回前 / 调用方法的call指令后
  3. 可能抛异常的位置

理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据,所以要尽量少放置safepoint,在safepoint会生成polling代码询问VM是否要“进入safepoint”,polling操作也是有开销的,polling操作会在后续解释。

通过 JIT 编译的代码里,会在所有方法的返回之前,以及所有 非counted loop 的循环(无界循环)回跳之前放置一个safepoint,为了防止发生 GC 需要 STW 时,该线程一直不能暂停。另外,JIT 编译器在生成机器码的同时会为每个safepoint 生成一些“调试符号信息”,为 GC 生成的符号信息是 OopMap,指出栈上和寄存器里哪里有 GC 管理的指针。

安全区域

对于非运行状态的线程(比如处在Sleep或者Blocked状态的线程),可能并不会在很短的时间内到安全点去。所以为了解决这个问题,引入了安全区域的概念。

安全区域很好理解,就是在程序的一段代码片段中并不会导致引用关系发生变化,也就不用去更新OopMap表了,那么在这段代码区域内任何地方进行GC都是没有问题的。这段区域就称之为安全区域。线程执行的过程中,如果进入到安全区域内,就会标志自己已经进行到安全区域了。那么虚拟机要进行GC的时候,发现该线程已经运行到安全区域,就与该线程的运行状态无关了。所以该线程在脱离安全区域的时候,要自己检查系统是否已经完成了GC或者根节点枚举(这个跟GC的算法有关系),如果完成了就继续执行,如果未完成,它就必须等待收到可以安全离开安全区域的Safe Region的信号为止。