OpenJDK 执行GC等全局操作前必须等待所有Java线程到达安全点,因仅在此处栈和寄存器引用状态稳定、可达性可精确判定;轮询缺失(如纯计算循环、native锁争用、Thread.onSpinWait)会导致线程长时间无法停靠,拖慢STW。

OpenJDK 必须等待所有 Java 线程进入安全点,才能执行 GC、类卸载、JIT 去优化等全局操作——这不是可选项,而是内存语义安全的硬性前提。
安全点是 JVM 唯一能准确扫描栈和寄存器的位置
线程在任意指令处被强制中断,可能导致栈帧未完成压入、对象引用处于中间状态(比如刚分配但字段还没初始化),此时 GC 无法判断哪些对象可达。安全点确保线程停在“引用关系已稳定”的位置:方法返回前、循环末尾、同步块出口等 JIT 或解释器明确插入轮询的位置。这些位置的栈帧结构完整,局部变量表和操作数栈中的对象引用都已就绪,可被精确遍历。
常见错误现象:Total time for which application threads were stopped 远大于 Stopping threads took,说明大部分时间花在等某个线程“走到路口”,而不是 GC 本身。
不是所有代码路径都会触发安全点轮询
JIT 编译器只在它认为“合理”的位置插入轮询逻辑;解释器则在每条字节码分发前检查 polling_word。但以下情况会导致轮询缺失或失效:
-
while(true) { /* 纯计算,无方法调用、无内存访问 */ }—— JIT 可能完全不插 poll,线程卡死在循环体中 -
String::intern()在高竞争下陷入 native 锁争用,而 native 方法默认不设 safepoint poll -
Thread.onSpinWait()显式提示 JVM “我正在自旋”,会抑制 poll 插入 - 长时间阻塞在 IO 或 JNI 调用中,返回前才检查一次,期间无法响应
如何确认是安全点等待拖慢了 STW
仅看 GC pause 日志没用,得开诊断参数抓真实耗时:
-
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1:输出每次 safepoint 的spin、block、sync阶段耗时 -
-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=1000:超 1 秒未到达的线程会被记录,日志里直接标出是哪个http-nio-8080-exec-xx线程卡住 -
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=jvm.log:配合上面参数导出完整上下文
关键指标看 Stopping threads took 占总停顿的比例——如果超过 90%,基本就是某几个线程长期游离在 safepoint 之外。
线程状态影响安全点响应能力
线程是否能快速响应,取决于它当前所处的状态:
- 运行在解释器或 JIT 编译代码中:每轮 dispatch 或循环回边都会检查
polling_word,响应快 - 运行在 native 方法中(如
System.arraycopy、FileInputStream.read):只在返回 Java 层前检查一次,期间不可中断 - 处于
_thread_blocked或_thread_in_native状态:JVM 不等它,直接跳过;但若它正持有锁或修改共享结构,可能间接拖慢其他线程进入安全点 - 在
synchronized块内且发生锁膨胀:偏向锁撤销、轻量级锁竞争失败升级为重量级锁的过程,涉及 VM 操作,本身就需要安全点
最隐蔽的坑是:你以为线程“在等锁”,其实它卡在 native 调用里,根本没机会检查 polling_word——这时候加再多 GC 线程也无济于事。


















