gc日志中secs字段即真实stw停顿时间,需结合gc类型、触发原因及内存分布分析;优先排查to-space exhausted、evacuation failure、concurrent mode failure三类诱因,并合理设置g1弹性参数而非固定年轻代大小。

直接看 GC 日志里的暂停时间字段,比任何监控工具都准;但只看数字没用,必须结合 GC 类型、触发原因和内存分布一起读。
怎么看 GC 日志里真实的停顿时间
Java 9+ 默认日志格式中,每行 GC 记录末尾的 secs 就是本次 STW 停顿耗时,例如:
[GC pause (G1 Evacuation Pause) (young), 0.0234567 secs]
这个 0.0234567 secs(约 23.5ms)就是真实世界停顿,不是估算值。注意以下三点:
- 日志中带
Full字样的(如Full GC或G1 Full GC)基本都 >500ms,属于严重问题,优先排查 -
Remark阶段(如CMS Remark或G1 Final Marking)是 CMS/G1 的关键 STW 环节,超 100ms 就需干预 - 同一行里如果有多个时间(如
user=0.042 sys=0.003 real=0.045),real才是真实停顿,user/sys是 CPU 时间,不能替代
停顿长 ≠ GC 算法不行,先查三类高频诱因
很多团队一看到停顿高就换 ZGC,结果发现是配置或代码问题。真正该先查的是:
-
to-space exhausted:G1 在年轻代回收时发现没有足够空闲 region 放晋升对象,强制退化为 Full GC —— 根本原因是-XX:G1ReservePercent设太低或堆碎片严重 -
evacuation failure:G1 复制对象时 Survivor 区放不下,大量对象提前进老年代 —— 往往因-XX:MaxTenuringThreshold过高或-XX:SurvivorRatio不合理 -
concurrent mode failure:CMS 在并发标记阶段老年代突然填满,被迫中断并发流程,降级为 Serial Old —— 典型信号是老年代使用率在 GC 前已 >90%
这些错误信息一定出现在 GC 日志的 Cause 字段或括号内,不 grep 就容易漏掉。
G1 下调优停顿最易踩的两个参数坑
G1 的 -XX:MaxGCPauseMillis 是目标值,不是保证值;盲目设小反而引发反效果:
- 设
-XX:MaxGCPauseMillis=50但堆里长期有 2GB 老年代对象,G1 会把年轻代压到极小(比如128MB),导致 Young GC 频率飙升,总停顿反而上升 - 显式设置
-Xmn或-XX:NewRatio会禁用 G1 的自适应年轻代调节,让MaxGCPauseMillis形同虚设 —— G1 文档明确要求“不要固定年轻代大小” - 真正该调的是
-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent(默认 5/60),给 G1 划定弹性区间,比如设为20和50,让它在负载变化时自主伸缩
车载/实时场景下停顿归因必须加 -XX:+PrintGCTimeStamps
普通业务日志用 -Xlog:gc*:file=gc.log 足够,但在车载等对抖动敏感的系统里,必须补上时间戳精度:
-
-XX:+PrintGCTimeStamps(Java 8)或-Xlog:gc,time(Java 11+)提供毫秒级 wall-clock 时间,能精准对齐 CAN 总线帧时间戳,确认某次 18ms 停顿是否恰好卡在 ADAS 控制周期内 - 单靠
-XX:MaxGCPauseMillis=5无法保证硬实时,必须配合-XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=adaptive,否则 ZGC 在 4KB page 系统上可能因 TLB miss 导致意外延迟 - 日志里出现大量
SoftReference回收(如soft refs cleared: 12345),说明-XX:SoftRefLRUPolicyMSPerMB默认值干扰了实时线程 —— 应设为0强制立即清理
GC 日志不是越全越好,而是要带着「这次停顿是否影响控制周期」的问题去读;一行日志背后,可能是线程池泄漏、String 拼接滥用,也可能是 Survivor 区被某个定时任务长期占满 —— 离开上下文谈数字,等于没分析。










