「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。
1 前言
这篇文章的灵感来源于《深入理解Java虚拟机3》这本周志明老师的书,里面有一部分内容讲述了Java安全点以及小米由此踩的坑。
2 简介
Java SafePoint俗称安全点,你可以简单理解为它记录了GC的引用。所以在GC的时候需要从SafePoint获取可以回收的对象地址。
那么既然有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
3 实战
上述代码创建了两个线程进行原子类的递增,并且由于循环数较大,在睡眠1s之后,必定还未执行完。
所以最终结果应该是先打印出内容:num = xx。 接着主线程结束,另外两个线程结束。但现实往往出乎意料!!!
从上图可以看到,最终结果是产生了GC的日志,并且最终等待两个线程循环结束才打印出来。
在我手一抖,把 int换成了long类型后,竟然达到了预期效果,真是天了噜了。
4 原因
在某度上进行一通搜索后,问题乖乖的浮出了水面(窜出了缝纫机乐队的画面😂😂)。
还记得上面所说的安全点吗?主线程执行过程中,需要进行清理(不一定是GC),这时候需要等待所有线程到达安全点从而获取清理的对象地址。
而在上文那个int循环中并没有放置安全点,导致主线程一直在等待循环结束进入安全点(其实这个说法并不正确,应该是stop the world,挂起了线程),所以才会在最后才打印出 num = 200000000。
那为什么换成long之后又可以了呢?
聪明的同学已经猜到:使用 long 将会被放置安全点。
5 补盲
5.1 安全点的选取
安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的。
那有人就会说了,上述的循环数特别大,这还不是长时间运行吗?很可惜,jvm在一定程度上是通过值类型进行判断的, int类型或范围更小的数据类型产生的循环被称为可数循环,默认是不会被放置安全点的。
而long类型或者范围更大的数据类型作为索引值的循环就被称为不可数循环,将会被放置安全点。
当然除了循环,像方法调用这种也可能被放置安全点。
5.2 为什么要将这些位置设为安全点
当然是为了避免长时间stw了。就像上面代码上的问题,等待了好久。如果是在企业的C端项目里,分分钟给你个3.25,只好含泪和年终奖说拜拜了!
6 解决方案
6.1 修改数据类型
如果你的循环是int,改为long。
6.2 增大循环周期(临时方案)
JVM不仅会在GC的时候使用safepoints,还有许多其他操作也使用 safepoint,
因此它会会周期性地停止 Java 线程。
周期由 -XX:GuaranteedSafepointInterval控制,该选项默认为 1000ms。
由于上述代码休眠了1s,那么我让安全点周期变成两秒,在进入安全点停止前 主线程就输出num。
注意这边需要两个命令一起用,不然连jvm都启动不了!!!
-XX:+UnlockDiagnosticVMOptions
-XX:GuaranteedSafepointInterval=2000
6.3 强制进入安全点
上面的int不放置安全点,其实是jvm的优化。
使用-XX:+UseCountedLoopSafepoints 这个配置时,可以强制关闭jvm的这个优化。
6.4 升级jdk
在jdk8中,这个问题还存在。而其他版本例如11和17中,这个问题已经被JVM优化过了。
7 扩展
小米踩坑始末
小米的公共离线HBase集群需要跑离线任务,但是呢有两个线程因为使用了int,导致其他线程都在等待进入safepoint。导致线程的STW时间可以到3秒以上。
要是上面都认真看了,相信也难不倒大家。
8 参考文章
jvm大局观之内存管理篇: 理解jvm安全点,写出更高效的代码
真是绝了!这段被JVM动了手脚的代码
《深入理解Java虚拟机》