真实晋升年龄以GC日志中动态计算的new threshold为准,而非MaxTenuringThreshold配置值;它每轮Minor GC后可能变化,受Desired survivor size与存活对象大小关系驱动。

看 GC 日志里的 new threshold,不是配置的 MaxTenuringThreshold
真实晋升年龄永远以 GC 日志中每次打印的 new threshold 为准。比如日志里出现:
Desired survivor size 1048576 bytes, new threshold 2 (max 15) - age 1: 2932120 bytes, 2932120 total
说明 JVM 判定:age 1 的对象总大小(2.9MB)已超过 Desired survivor size(1MB),于是把所有 age ≥ 1 的对象全部晋升——哪怕你配了 MaxTenuringThreshold=15,这次也完全失效。
关键点:
-
new threshold是动态计算结果,每轮 Minor GC 后都可能变 - 它只在
-XX:+PrintGCDetails开启后才输出,不加这个参数就看不到 - 别 grep “MaxTenuringThreshold”,要 grep “
new threshold” 或 “age”
短命大对象卡在 Survivor 区的根本原因是 Desired survivor size 不足
短命大对象(比如单次分配几 MB 的临时 byte[])如果跨过一次 GC 还没死,就会进入 Survivor;但它们体积大、数量少,极易撑爆 Survivor 空间,触发动态年龄判定提前晋升。
这时调高 MaxTenuringThreshold 没用,真正该做的是让这类对象压根别进 Survivor:
- 用
-XX:PretenureSizeThreshold直接拦截:设为略大于你最大短命对象的尺寸(如-XX:PretenureSizeThreshold=2M),≥2MB 的对象直接进老年代 - 同时检查
-XX:SurvivorRatio:若设得过大(如 16),Survivor 实际容量太小,Desired survivor size更难满足;建议从-XX:SurvivorRatio=6起步 - 确认是否真“短命”:用
jstat -gc <pid></pid>观察S0U/S1U在 GC 后是否快速回落——如果不回落,说明对象没死,那就不是“短命”,而是存活率高,该查代码逻辑
Parallel GC 下 MaxTenuringThreshold 有效,G1 下基本无效
如果你用的是 -XX:+UseG1GC,MaxTenuringThreshold 几乎不参与晋升决策。G1 用的是区域复制 + 年龄预测模型,不依赖固定年龄阈值。
而 Parallel GC(即 -XX:+UseParallelGC)会严格按动态计算出的 new threshold 执行晋升,且受 MaxTenuringThreshold 上限约束。
所以先确认收集器:
-
java -XX:+PrintGCDetails -version启动时看日志头,找Using Parallel或Using G1 - 如果是 G1,别花时间调
MaxTenuringThreshold,转去调-XX:G1MixedGCCountTarget或-XX:G1HeapWastePercent - Parallel GC 下,若
new threshold长期 ≤ 2,说明 Survivor 容量或对象存活率有问题,不是参数数字写得不够大
验证调整是否生效,必须连续观察多轮 GC 日志
单次 GC 日志看不出趋势。比如看到一次 new threshold=7,下一轮又掉到 2,说明问题没解决,只是偶然波动。
正确做法:
- 开启完整 GC 日志:
-Xlog:gc*:file=gc.log:time,uptime,level,tags(JDK 9+)或-XX:+PrintGCDetails -XX:+PrintGCTimeStamps(JDK 8) - 用
grep "new threshold" gc.log | head -n 20抽前 20 次,看数值分布 - 重点关注
Desired survivor size和实际S0U/S1U的比值:长期 >90%,说明 Survivor 持续淤积,晋升路径已被迫压缩
最常被忽略的一点:动态阈值是基于 Survivor 当前可用空间算出来的,不是基于你写的 SurvivorRatio 或 Xmn 推导的理论值。实际空间受 GC 算法、对象分布、TLAB 分配策略共同影响,必须靠日志反推,不能靠配置倒推。

















