Jvm垃圾回收GC Root与安全点Safepoint

3,023

我看很多资料在介绍GC Root时,并没有说栈帧的操作数栈上引用的对象也是GC Root,包括我去翻阅《深入理解Java虚拟机》这本书也是一样。所以我才好奇。

为什么我会觉得操作数栈上引用的对象也应该是GC Root节点?

假设在垃圾回收标记阶段,由于并发标志(如cmsg1),此时如果用户线程在方法中new一个对象,执行new字节码指令时,new出来的对象的引用是保存在操作数栈顶的,此时并未保存回本地变量表,也并不一定要保存回本地变量表,那么这个新new的对象岂不是会被回收。

New一个对象,并不一定会保存回本地变量表,如

public class Main {

    public void hello(){
        System.out.println("wujiuye");
    }

    public static void main(String[] args) {
        new Main().hello();
    }

}

创建出来的对象Main是不会存储到本地变量表的。 字节码如下:

带着疑问,我翻阅GC垃圾回收知识点相关资料,才想起Safepoint。如果说安全点在方法执行之前或之后,就不需要将操作数栈引用的对象作为GC Root

关于安全点Safepoint:

1、已经挂起的线程处于安全点(安全区域),如WAITBLOCK状态。只有处于running状态的线程需要等待执行到安全点。 2、方法调用指令是安全点,即被调用的方法执行之前。方法结束出口处是安全点,如ruturn指令之前。 3、循环体在进入下一次之前是安全点。因为很多循环都是耗时操作,如循环一百万次,jvm等待多个线程执行完循环再进行垃圾回收,那么进程就会处于假死状态。

第三点循环体结束进入下一次之前,也可以把循环体当作一个方法,无论这个方法做什么事情,这个方法每次执行结束都不会存在创建对象未保存回本地方法表的情况。因此,没有必要考虑将操作数栈引用的对象作为GC Root,证明了我的想法是错误的。

JIT执行方式下,JIT编译的时候直接把Safepoint的检查代码加入了生成的本地代码。当JVM需要让Java线程进入Safepoint时,只需要设置一个标志位,让Java线程运行到Safepoint时主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。如HotSpotx86中为轮询Safepoint会生成一条类似于test汇编指令。

使用JITWatch工具,可以验证。JITWatch还提供了一个测试保存点的demo:SafePointTest。因此连demo都不需要写了。关于JITWatch可以看下我前面写的这篇文章《JITWatch查看字节码被JIT编译后的汇编代码》。

可以看到,incCounter方法反编译为汇编代码后,在retq这条返回指令之前插入了一条test指令,也是在return字节码指令处。

既然说到这,我们也一并分析循环体在每进入下一次循环之前插入Safepoint的检查。

从图中可以看出,对应字节码是goto指令的地方,也就是每一次循环结束的地方,都会判断是否需要跳转执行text指令(汇编)。

这里还可以引申出一个知识点,就是逃逸分析。也是为了提升性能。JIT即时编译器会将多次被执行的字节码编译为机器码,同时也会分析方法体内的对象创建。如果方法体内创建的对象没有逃离出方法体之外,即不会被别的地方引用,没有别的线程使用,那么就不需要将对象分配到堆中,而是直接分配到虚拟机栈上。当对象分配在虚拟机栈上,对象的生命周期就是对象被创建到方法执行结束,随着栈帧的出栈而灭亡。

还记得Jvm调优参数-Xss吗,这是配置虚拟机栈的大小,默认为1m大小,在Linux 64位操作系统下最小为228kb,一般设置为最小256kb。该值的设置需要考虑线程上方法调用的深度,以及方法调用栈上每个方法的操作数栈的深度和本地变量表的长度。如果存在因逃逸分析而将对象分配在栈上的可能,还需要将此估算进栈的大小。

图中的代码片段是我从之前我写的一个异步框架截取的,使用asm生成字节码,通过调用Method VisitorvisitMaxs方法设置操作数栈的深度和本地变量表的长度。也就是说java代码在编译成字节码之后,操作数栈的深度和本地变量表的长度就已经是确定的了。

你们在学习垃圾回收的时候,有没有想过为什么gc回收时要停止所有用户线程呢?

如果停留在表面理解,gc回收后需要为解决内存碎片为,需要整理内存,如Parallel Old的老年代回收,使用标志-整理算法。整理意味着对象的地址会改变,因此gc后需要修正对象的引用。再如,对象从新生代进入老年代。

CMS并发标志也会有STW,在初始标志和重标志两个阶段。因为被标志的对象还会有被用户线程修改引用的可能,而在重标志阶段就不得不重新遍历那些已经修改过引用的对象。只是可以降低总的STW时间。更深层次的还要去了解卡表、写屏障。