
为什么 GC 停顿(STW)总比预期长?
STW 时间长,往往不是因为堆太大,而是 GC 策略和对象生命周期不匹配。比如用 G1GC 却没调优 MaxGCPauseMillis,或老年代里堆着大量短期存活的中等对象,触发频繁混合回收;又或者用了 ParallelGC 处理响应敏感服务,它天生以吞吐优先,单次 STW 就可能飙到几百毫秒。
关键判断:先看 GC 日志里 Pause 类型和耗时分布——是 G1 Evacuation Pause 主导?还是 Concurrent Mode Failure 强制退化成 Full GC?前者说明 G1 没控住停顿,后者说明并发标记跟不上分配速率。
怎么选对 GC 算法并配准核心参数?
别无脑跟风 ZGC 或 Shenandoah,它们在 JDK 11+ 才成熟,且对大堆(>8GB)、低延迟(G1GC 仍是更稳的选择。
-
-XX:+UseG1GC是前提,必须显式开启 -
-XX:MaxGCPauseMillis=30设目标值(非保证值),G1 会据此动态调整年轻代大小和混合回收频率;设太低(如 10)反而导致更频繁的 GC 和更高 CPU 开销 -
-XX:G1HeapRegionSize=1M(默认自动推导)——若对象普遍 >512KB,手动设大一点可减少跨区引用,降低 Remembered Set 维护开销 - 禁用
-XX:+UseStringDeduplication(除非确认有海量重复字符串),它会在 GC 期间额外加锁扫描,延长 STW
哪些代码习惯会偷偷拖长 STW?
GC 停顿不是纯 JVM 黑盒问题,应用层行为直接影响 GC 效率。最常见的是“隐式晋升”:本该在年轻代回收的对象,因 Survivor 区过小或 MaxTenuringThreshold 设太高,被提前送入老年代;后续老年代碎片化,触发 Concurrent Mode Failure。
立即学习“Java免费学习笔记(深入)”;
- 避免在循环里拼接超长
StringBuilder后反复.toString(),生成大量临时大对象,容易直接分配进老年代(-XX:+AlwaysTenure关闭时也难逃) - 慎用
ThreadLocal存放大缓存对象,线程复用下易造成内存滞留,GC 时需扫描整个 ThreadLocalMap - 日志框架如 Logback 的
%X{traceId}若绑定非常驻 MDC,每次请求都 new 一个 Map,会显著抬高年轻代分配速率
怎么验证调优是否真生效?
别只盯着 GC 日志里的平均停顿,要抓毛刺。用 jstat -gc -h10 <pid> 1000</pid> 持续采样,观察 G1YGC 和 G1FGC 的频次与耗时波动;更关键的是用 -Xlog:gc*,gc+phases:file=gc.log:time,tags:uptime(JDK 10+)打开详细阶段日志,重点看 Evacuation 阶段里 Root Region Scan 和 Remembered Set 处理是否占大头——如果是,说明跨代引用多,得优化对象图结构或调大 G1RSetUpdatingPauseTimePercent。
真正容易被忽略的点:JVM 参数之间存在隐性冲突。比如同时设了 -XX:MaxGCPauseMillis=20 和 -XX:G1NewSizePercent=40,G1 可能因年轻代被锁死而无法动态收缩,反而让停顿失控。调参永远从单一变量开始,配合真实流量压测,而不是靠理论公式拍定。










