关键看GC日志中“new threshold”实际值,它由JVM每轮Minor GC后根据Survivor区各年龄对象累计占比动态计算得出,而非固定依赖MaxTenuringThreshold;例如“new threshold 2 (max 6)”表示年龄≥2的对象将晋升,该值可能逐轮变化。

看 GC 日志里 new threshold 实际值才是关键
动态年龄判定不是靠 MaxTenuringThreshold 静态生效的,而是每轮 Minor GC 后 JVM 根据 Survivor 区内各年龄对象累计占比重新计算。真正起作用的是日志中这行:Desired survivor size 524288 bytes, new threshold 2 (max 6)。其中 new threshold 2 表示本次 GC 后,年龄 ≥2 的对象将直接晋升老年代。
这个值每轮都可能变:如果某次 GC 后,年龄 1 的对象就占了 Survivor 总容量的 55%,那下一轮 new threshold 就会变成 1 —— 所有存活对象只要活过一轮就进老年代。
常见误判点:
- 只查
MaxTenuringThreshold配置,却忽略日志里实际new threshold长期是 1 或 2 - 看到
new threshold 6 (max 6)就以为“没触发动态机制”,其实更可能是 Survivor 空间太宽松,对象根本没机会堆满 - 把
TargetSurvivorRatio(默认 50%)当成 Survivor 使用率目标,误以为 JVM 在“努力填到 50%”
SurvivorRatio 过小会放大动态阈值波动
当设置 -XX:SurvivorRatio=2(即 Eden : S0 : S1 = 2 : 1 : 1),每个 Survivor 仅占年轻代 25%,空间非常紧张。此时两个连锁效应会同时发生:
- Minor GC 后,哪怕只多几百 KB 存活对象,To Survivor 就装不下 → 直接晋升老年代(非年龄原因)
- Survivor 内年龄分布稍一集中(比如一批请求生成的 DTO 全是年龄 1),累计占比轻松超 50% →
new threshold骤降到 1 - 老年代在短时间内接收大量本该留在年轻代的对象,可用空间快速下降,触发
Concurrent Mode Failure或前置 Full GC
实测中,SurvivorRatio=2 在秒杀类流量脉冲场景下,Full GC 频率可能从几小时一次飙升至每分钟多次。
用 jstat -gc 快速定位是否因 Survivor 不足导致提前晋升
运行 jstat -gc <pid> 1000</pid> 观察关键字段变化趋势:
-
S0U和S1U长期接近 0,但OU(老年代使用量)持续稳步上升 → Survivor 区几乎没起到缓冲作用 -
EC(Eden 容量)和EU(Eden 使用量)频繁打满,YGC次数高,但YGCT却不高 → Minor GC 勤快但效率低,大量对象“秒进老年代” - 对比
S0C/S1C(Survivor 容量)与S0U/S1U峰值,若后者常达前者的 90%+,说明 Survivor 区已成瓶颈
注意:jstat 不显示年龄分布,它只能告诉你“Survivor 是否被压垮”,不能替代 GC 日志分析 new threshold。
调整 SurvivorRatio 时必须配合 -Xmn 固定新生代大小
如果不固定新生代大小,启用 -XX:+UseAdaptiveSizePolicy(默认开启)会导致 JVM 动态收缩 Survivor 区——你刚调大 SurvivorRatio,它可能立刻把整个新生代也缩小,结果 Survivor 绝对空间反而没变。
安全做法是:
- 先用压测确定应用典型对象生命周期(平均存活几轮 GC)
- 设
-Xmn1g -XX:SurvivorRatio=4,让每个 Survivor 有约 128MB(1g × 1/8) - 再开
-XX:+PrintGCDetails,盯住后续几天的 GC 日志,确认new threshold是否稳定在 3~4,且OU增速明显放缓
最容易被忽略的一点:动态年龄判定依赖 UseAdaptiveSizePolicy,但这个策略本身又会让 JVM “自作主张”调整空间。想稳住 Survivor 缓冲能力,就得主动关掉它的自适应权——用 -Xmn 锁死新生代,才是真实可控的起点。










