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

原创 吴就业 125 0 2020-01-28

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/78badde3bbe740e2b139f1035bee43f9

作者:吴就业
链接:https://wujiuye.com/article/78badde3bbe740e2b139f1035bee43f9
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2020年01月28日,从公众号同步过来(博客搬家),本篇为原创文章。

我看很多资料在介绍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是不会存储到本地变量表的。字节码如下:

创建出来的对象Main是不会存储到本地变量表的

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

关于安全点Safepoint:

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

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

使用JITWatch工具,可以验证。JITWatch还提供了一个测试保存点的demo:SafePointTest。因此连demo都不需要写了。

使用JITWatch查看汇编代码

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

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

每进入下一次循环之前插入Safepoint的检查

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

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

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

asm设置操作数栈的深度和本地变量表的长度

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

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

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

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

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

64位JVM的Java对象头详解

在学习并发编程知识`synchronized`时,我们总是难以理解其实现原理,因为偏向锁、轻量级锁、重量级锁都涉及到对象头,所以了解`java`对象头是我们深入了解`synchronized`的前提条件。

Java锁事之Unsafe、CAS、AQS知识点总结

Unsafe、CAS、AQS是我们了解Java中除synchronized之外的锁必须要掌握的重要知识点。CAS是一个比较和替换的原子操作,AQS的实现强依赖CAS,而在Java中,CAS操作需通过使用Unsafe提供的方法实现。

Dubbo路由功能实现灰度发布及源码分析

灰度发布是实现新旧版本平滑过渡的一种发布方式,即让一部分服务更新到新版本,如果这部分服务没有什么问题,再将其它旧版本的服务更新。而实现简单的灰度发布我们可以使用版本号控制,每次发布都更新版本号,新更新的服务就不会调用旧的服务提供者。

服务提供者假死,记一次full gc问题排查

线上某服务一直运行很稳定,最近突然就`cpu`百分百,`rpc`远程调用全部失败,并走了`mock`逻辑。重启后,一个小时后问题又重现。于是`dump`线程栈信息,但不仔细看也看不出什么问题。于是就有了一番排查历程。

JITWatch查看字节码被JIT编译后的汇编代码

最近看书看到关于`volitale`关键字与`jmm`内存模型的介绍,这个知识点似乎看了好多次,背都能背下来了。但理论性的东西真的很容易忘记,看不到摸不着。于是乎,我上网搜索看底层机器指令的实现,发现不少文章说可以看到`java`编译后的汇编代码,于是了解到`jitwatch`这个工具,从名字上也能看出`jit`编译器监视的意思。

Spring Cloud Kubernetes入门必知运维知识之Docker

容器化部署就是一次配置到处使用,例如将安装nginx配置nginx这一系列工作制作成一个镜像,在服务器上通过docker拉取镜像并启动容器即可完成nginx的部署。