zgc真实stw时间即日志中带[pause]标签的pauseinitialmark和pausefinalcleanup耗时,单位毫秒,需结合gc(id)对齐周期,非估算值。

ZGC 日志里能直接看到的 PauseInitialMark 和 PauseFinalCleanup 就是真实发生的 Stop-The-World 时间,不是估算值,也不依赖 -XX:+PrintGCApplicationStoppedTime 这类辅助参数——只要日志里有 [Pause] 标签,就代表一次 STW。
怎么看 ZGC 日志里的真实 STW 时间
ZGC 的 STW 阶段只有两个:初始标记(PauseInitialMark)和最终清理(PauseFinalCleanup),其他阶段全是并发执行。日志中每行带 [Pause] 的记录就是可测量的停顿点。
- 必须启用
-Xlog:gc*,gc+z=info,否则gc+z=info不生效,看不到Pause*事件 - 时间单位是毫秒,但日志里不写单位,比如
PauseInitialMark2ms表示 2 毫秒,不是 2 秒或 2 微秒 - 不要用
grep "Pause"粗筛后直接相加——同一 GC 周期可能触发多次PauseInitialMark(如并发标记失败回退),需结合GC(ID)字段对齐周期 - 如果日志里完全没出现
Pause字样,说明没启用 ZGC,或者用了-XX:+UseG1GC却误以为在看 ZGC 日志
为什么 MaxPauseMillis=10 不等于每次暂停都 ≤10ms
-XX:MaxPauseMillis=10 是 ZGC 的目标值,不是硬性上限。它影响的是 GC 触发频率和并发线程数分配,而不是单次暂停的强制截断。
- 当堆内对象图复杂(如大量跨代引用、finalizer 链过长),
PauseInitialMark可能短暂突破 10ms,这是正常行为 - 该参数只在 JDK 15+ 生效;JDK 11–14 中该参数被忽略,实际停顿由堆大小和并发线程数隐式决定
- 设得太低(如
=5)会导致 ZGC 更频繁地启动并发标记,反而增加 CPU 开销和内存扫描压力 - 真正影响单次暂停时长的是对象根集(root set)规模,而非堆总大小——所以减少全局静态集合、避免滥用
ThreadLocal引用链才是治本
对比 G1 时别混淆两类 STW 来源
G1 的 STW 时间不能只看 Pause 开头的日志,因为它的 STW 分散在多个环节:Young GC、Mixed GC、Humongous 分配失败、Full GC,且部分日志默认不输出详细阶段。
- G1 下
-Xlog:gc*=info输出的Pause Young或Pause Mixed才是 STW,但它们包含整个 GC 周期的暂停,无法拆解出“仅 mark root”耗时 - 若想定位 G1 中哪部分最拖慢 STW,必须加
-Xlog:safepoint,观察 safepoint 达成延迟是否远高于 GC 本身耗时——很多“假 STW”其实是应用线程卡在循环或 native 调用里进不了 safepoint - ZGC 的
PauseInitialMark耗时稳定在 0.5–3ms(典型值),而 G1 的Pause Young在 20–80ms 波动,二者不可直接按数值对比 - 别用
-XX:+PrintGCApplicationStoppedTime测 ZGC:它统计的是 JVM 停止所有线程的总时长,会把 safepoint 等待、JIT 编译暂停等全算进去,和 ZGC 自身的 STW 无关
调优时最容易被忽略的三个点
STW 时间短 ≠ 应用无感知。ZGC 的 PauseInitialMark 虽然只有几毫秒,但如果每秒发生 20 次,累积抖动仍会影响 P99 延迟。
- 高频小停顿比低频长停顿更难被监控工具捕获,建议用
awk '/PauseInitialMark/ {print $NF}' sdk_zgc.log | sed 's/ms$//' | awk '{sum += $1; n++} END {print sum/n}'算均值,再结合直方图看分布 - 如果
PauseFinalCleanup明显变长(如从 0.3ms → 5ms),大概率是老年代对象被大量晋升后未及时回收,检查是否有缓存未设置淘汰策略或WeakReference被批量强引用 - ZGC 不处理 Metaspace,但 Metaspace GC 会触发独立 STW,且日志不归在
gc+z下——必须单独加-Xlog:metaspace=info排查










