bigywater.tech
Java · 236

Safepoint:所有线程为什么会短暂停下来

所有应用线程在 GC 等操作时都要先「停到安全点」。停下来要多久,取决于最慢的那个线程什么时候遇到安全点检查。这篇用真实的日志和一个故意去掉安全点检查的计数循环,看到达安全点的时间怎么变成一个线程拖住所有线程。

0 → 1100 ms
getAllStackTraces:默认 → 去掉计数循环安全点检查
25
一个小程序里 -Xlog:safepoint 记录的安全点次数
0
空闲 JVM 6 秒内的安全点次数

安全点日志:原因与耗时

实测 · 本机执行输出
  分配 400 MB 小对象 + System.gc() + 3 次 Thread.getAllStackTraces() 完成
  本次运行共 25 次安全点;按原因统计(次数 / 到达安全点耗时中位 μs / 在安全点内耗时中位 μs):
    G1CollectForAllocation        11 次  到达     12.9 μs  在安全点内     267.1 μs
    G1TryInitiateConcMark          4 次  到达      0.7 μs  在安全点内     293.6 μs
    G1PauseRemark                  3 次  到达      1.3 μs  在安全点内     167.9 μs
    G1PauseCleanup                 3 次  到达     38.1 μs  在安全点内      34.2 μs
    ThreadDump                     3 次  到达      0.2 μs  在安全点内       8.2 μs
    G1CollectFull                  1 次  到达      9.2 μs  在安全点内    1148.8 μs

计数循环与到达安全点

实测 · 本机执行输出
  [默认(JDK 21,G1)]
  工作线程在 JIT 编译后的 int 计数循环里(内层 20 亿次迭代、外层死循环),主线程调用 Thread.getAllStackTraces()(需要全局安全点)8 次的耗时(ms):[0, 0, 0, 0, 0, 0, 0, 0]
  再调用 System.gc() 耗时 1 ms
    日志里「到达安全点」最慢的 3 次: [('ThreadDump', '0.1 ms'), ('ThreadDump', '0.1 ms'), ('ThreadDump', '0.0 ms')]
  [-XX:LoopStripMiningIter=0 -XX:-UseCountedLoopSafepoints]
  工作线程在 JIT 编译后的 int 计数循环里(内层 20 亿次迭代、外层死循环),主线程调用 Thread.getAllStackTraces()(需要全局安全点)8 次的耗时(ms):[1100, 1067, 1066, 1065, 1062, 1070, 1065, 1064]
  再调用 System.gc() 耗时 1066 ms
    日志里「到达安全点」最慢的 3 次: [('ThreadDump', '1099.8 ms'), ('ThreadDump', '1070.4 ms'), ('ThreadDump', '1066.9 ms')]

各种操作的停顿

实测 · 本机执行输出
  空闲 1 秒(基线)                           耗时  1002 ms:ticker 线程(每 1 ms 醒一次)最大延迟 0.6 ms,延迟 ≥1 ms 的次数 0,≥5 ms 0,≥20 ms 0
  1000 次 getThreadInfo(单个线程,栈深 1)      耗时    23 ms:ticker 线程(每 1 ms 醒一次)最大延迟 0.5 ms,延迟 ≥1 ms 的次数 0,≥5 ms 0,≥20 ms 0
  200 次 Thread.getAllStackTraces()     耗时    10 ms:ticker 线程(每 1 ms 醒一次)最大延迟 0.5 ms,延迟 ≥1 ms 的次数 0,≥5 ms 0,≥20 ms 0
  5 次 System.gc()                      耗时    15 ms:ticker 线程(每 1 ms 醒一次)最大延迟 2.9 ms,延迟 ≥1 ms 的次数 5,≥5 ms 0,≥20 ms 0
  这次运行的日志里全局安全点按原因统计(getThreadInfo 与 getAllStackTraces 都记为 ThreadDump): {'Cleanup': 1, 'ThreadDump': 1200, 'G1CollectFull': 5}

空闲 JVM

实测 · 本机执行输出
  空闲 6 秒结束
  空闲 6 秒里日志里的安全点共 0 次: []
  加 -XX:GuaranteedSafepointInterval=0(关闭周期性安全点)后空闲 6 秒:0 次
  加 -XX:GuaranteedSafepointInterval=200 后空闲 6 秒:0 次;原因  []

分步演示

Safepoint
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,G1,10 核 macOS
    • 没有验证:ZGC/Shenandoah 的握手(handshake)与安全点的区别、JNI 临界区对安全点的影响(见 GCLocker 相关篇目)、JDK 17 与 27 的行为差别、-XX:LoopStripMiningIter 取其它值的效果、真实应用里长循环的 TTSP 分布;-XX:-UseCountedLoopSafepoints 是故意制造问题的实验开关,不要用在生产
    京ICP备2026067502号-1京公网安备11010502063487号