摘要:某天夜里,公司的服务挂了几分钟,由于服务自动重启丢失了现场,没有排查线索,于是领导建议我写个脚本监测 cpu,内存使用率,在占用率高时,使用 jstack,jmap 保存现场信息。
某天夜里,公司的服务挂了几分钟,由于服务自动重启丢失了现场,没有排查线索,于是领导建议我写个脚本监测 cpu,内存使用率,在占用率高时,使用 jstack,jmap 保存现场信息。
于是... 过了几天,服务又挂了,经过排查,是因为监测脚本有问题,在没问题的时间节点执行了 jmap, 导致 jvm 长时间卡顿。但是为什么,执行了 jmap 会导致 jvm 卡顿呢,这就要说到本期要讲的 Safepoint 了
Safepoint(安全点) 是指程序执行过程中一个特定的位置,在这个位置上 JVM 能安全地暂停线程,以执行某些需要全局一致性操作的任务(如 GC)。换句话说,只有当所有线程都到达了 Safepoint,JVM 才能安全地执行某些操作。
区别于初识安全点的时候局限于 GC 中的安全点概 #技术分享念,这里给安全点一个比较全面的定义:
Safepoint 可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停。在 SafePoint 保存了其他位置没有的一些当前线程的运行信息,供其他线程读取。这些信息包括:线程上下文的任何信息,例如对象或者非对象的内部指针等等。我们一般这么理解 SafePoint,就是线程只有运行到了 SafePoint 的位置,他的一切状态信息,才是确定的,也只有这个时候,才知道这个线程用了哪些内存,没有用哪些;并且,只有线程处于 SafePoint 位置,这时候对 JVM的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,之后继续运行,每个线程都有一份自己的内存使用快照,这时候其他线程对于内存使用的修改,线程就不知道了,只有再进行到 SafePoint 的时候,才会感知。
JVM 中需要全线程一致状态的操作都依赖 Safepoint,比如:
| 使用场景 | 描述 | | ---
| 垃圾回收(GC) | Stop-The-World 停顿前需等待所有线程进入 safepoint。| | Deoptimization | JIT 优化失败时需要回退到解释模式,也需要线程暂停。| | 偏向锁撤销 | 偏向锁被其他线程竞争时需要撤销,期间不能有线程执行相关代码。| | 类卸载(Class Unloading) | 清理无用类时需暂停所有线程。| | 线程堆栈分析 | JFR(Java Flight Recorder)或诊断工具需要稳定的栈信息。一些 jvm 自带工具:jstack jmap 等 | | jvm 默认轮询进入 | 每经过-XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint |
JVM 想让线程暂停,自己却不能直接中断线程,那该怎么办?
答案是:插点 + 协议暂停 。
JVM 会在程序运行时自动在一些“合适的地方”插入 safepoint 位置,通常包括:
方法调用前loop 回跳处分支跳转处触发异常的地方当 JVM 触发一次 safepoint 请求时:
JVM 设置一个 全局标志位 ,表示“要停一下了”;所有线程在执行到 safepoint 位置时, 主动检查该标志位 ;如果发现有 safepoint 请求,就“乖乖停下”,直到 JVM 操作完成;等操作做完后,再恢复执行。线程不是随时都能停,而是“下一个 safepoint 见”。
以下场景会触发 JVM 进入 safepoint:
GC(垃圾回收)包括 Minor GC、Major GC、Full GC,只要需要 STW(Stop-The-World),就必须进入 safepoint。
JVM(jdk1.8)有个默认参数 GuaranteedSafepointInterval, 这个参数默认是开启的,每经过-XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻 Stop the world 的任务执行,可以设置-XX:GuaranteedSafepointInterval=0关闭这个定时
当我们用 jstack 去 dump 线程栈,或者用 jmap 做内存分析时,JVM 为了确保信息一致性,也会让所有线程停在 safepoint。
在运行时动态插入代码(如使用 javaagent ),为了保证字节码修改不影响运行状态,也会强制进入 safepoint。
偏向锁撤销锁大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。但是高并发的情况下,偏向锁会经常失效,导致需要取消偏向锁,取消偏向锁的时候,需要 Stop the world,因为要获取每个线程使用锁的状态以及运行状态
当发生 JIT 编译优化或者去优化,需要 OSR 或者 Bailout 或者清理代码缓存的时候,由于需要读取线程执行的方法以及改变线程执行的方法,所以需要 Stop the world
因为线程必须等运行到 safepoint 才能停下,如果线程在执行过程中迟迟没有检查 safepoint,就会出现“卡住很久”的现象。
比如,某些长时间不触发方法调用的死循环 :
while(true) { i++;}这段代码就可能永远都不进入 safepoint,导致其它线程迟迟不能停,最终出现 STW 卡顿。
避免过长的“无方法调用”的循环逻辑 ;可以通过 -XX:+PrintSafepointStatistics 查看卡顿位置;使用 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation 查看哪些方法编译成了本地代码;避免过多频繁地使用 jstack 、 jmap ,尤其在高并发场景下。我们写段代码来观察线程在运行期间的行为:
package com.vv;import java.util.concurrent.atomic.AtomicInteger;public class Test {public static AtomicInteger counter = new AtomicInteger(0);public static void main(String args) throws Exception { long startTime = System.currentTimeMillis;Runnable runnable = -> { System.out.println(interval(startTime) + "ms 后," + Thread.currentThread.getName + "子线程开始运行");for (long i = 0; i示例代码中 主线程 启动两个 子线程 ,然后 主线程睡眠1s ,通过打印时间来观察主线程和子线程的执行情况。
按照预期,主线程会在1s 左右后,打印日志,实际情况却是:
主线程在2.3s 后才打印
先说结论由于 VMThread 的某些操作需要 STW,主线程在 sleep 结束前进入了 JVM 全局安全点,然后主线程要等待其他线程全部进入安全点,所以主线程被长时间没有进入安全点的其他线程给阻塞了,即两个子线程。
前文中我们讲过进入安全的点场景,其中有一个 jvm 默认轮询进入 的场景,就是因为这个,才导致所有线程进入 safepoint
验证通过 -XX:GuaranteedSafepointInterval = 0 关闭定时进入安全点,看看代码运行结果是怎么样的,注意,这个参数要结合 XX:+UnlockDiagnosticVMOptions 一起使用。
此时,我们看到,主线程按照预期,在1s 后打印了
通过 Safepoint 实现源代码:Safepoint.cpp 里的注释,我们可以发现
当执行 native 方法时,会进入 safepoint,而 Thread.sleep 就是 native 方法。
先看看一个来自 RocketMQ(org.apache.rocketmq.store.logFile.DefaultMappedFile#warmMappedFile) 代码里面的 for 循环,在循环里面,专门有个变量 j,来记录当前循环次数。
第一次循环以及往后每 1000 次循环之后,进入一个 if 逻辑。
作者真实的目的是为了在这里放置一个安全点,避免 for 循环运行时间过长导致系统长时间 STW
现在已经知道了主线程为什么进入会进入安全点,以及主线程在哪里进入的安全点,按照已知知识点 JVM 会在循环跳转处和方法调用处放置安全点,为什么子线程没有进入安全点?
JVM为了避免安全点过多带来过重的负担,对循环有一项优化措施,认为循环次数较少的话,执行时间应该不会太长,所以使用int类型和范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环,相对应的,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环,将被放置安全点。
在示例代码中,子线程的循环索引值数据类型是 int,也就是可数循环,所以 JVM 没有在循环跳转处放置安全点。把循环索引值数据类型改成 long 型,循环成为不可数循环,就能够成功在循环跳转处放置安全点,避免子线程长时间无法进入安全点阻塞主线程。
说到上文提到的Rocketmq源码,从代码的变更记录看,22年9月份有人对这段代码换了一种写法:把for循环变量类型定义成long型,同时注释掉了循环内部Thread.sleep(0)代码, 也是由于jvm认为范围更大的数不可循环,因此需要放置安全点
总结Safepoint 是 JVM 中用来协调线程暂停的“集结点”。GC、类卸载、诊断工具调用等操作都依赖于它。虽然听起来只是 JVM 的“内部细节”,但在系统调优、排查性能问题时,safepoint 就是你必须认识的老朋友 。
记住几个关键点:
JVM 停线程不是强制中断,而是等线程跑到 safepoint 再停;长时间不进入 safepoint 会导致 STW 卡顿;频繁触发 safepoint 会拖累系统整体性能;实战排查时,善用 jstack 、 PrintSafepointStatistics 等工具洞察本质。最后来源:墨码行者