GC日志中age行关键看动态晋升阈值(new threshold)及各龄对象大小与Desired survivor size的对比,而非MaxTenuringThreshold配置值;若age 1常超目标容量,说明Survivor区不足,需调小SurvivorRatio或扩容新生代。

怎么看 GC 日志里的 age 行
关键不是看 MaxTenuringThreshold 配了多少,而是每次 Minor GC 后日志里打印的那几行 age 统计。打开 -XX:+PrintGCDetails 后,你会看到类似:
Desired survivor size 1048576 bytes, new threshold 7 (max 15) - age 1: 2932120 bytes, 2932120 total - age 2: 1048576 bytes, 3980696 total - age 3: 524288 bytes, 4504984 total
这里 new threshold 7 才是本次 GC 实际生效的晋升年龄阈值,它由 JVM 动态计算得出,优先级远高于你配的 max 15。
真正要盯住的是:Desired survivor size(目标 Survivor 容量)和各 age N 行的字节数。只要某一行的单龄对象大小超过前者,JVM 就会把该龄及更老的对象全部晋升——这就是“动态年龄判定”在起作用。
为什么 new threshold 总是 1 或 2
这说明 Survivor 区严重不够用,不是对象“活得太久”,而是它们根本没地方待。常见原因包括:
-
SurvivorRatio设得太大(比如-XX:SurvivorRatio=12),导致单个 Survivor 区只有新生代的 1/14,远低于常规的 1/8~1/10 - 新生代整体太小,Eden 区一满就 GC,但存活对象总量已经逼近甚至超过一个 Survivor 区容量
- 应用存在大量“中等寿命”对象(比如缓存、DTO、临时集合),它们跨 1~2 次 GC 还没死,又不够大到触发大对象直入老年代
此时调高 MaxTenuringThreshold 没用——因为 new threshold 被动态压到了 1,配置的 15 根本不会触发。
怎么判断该调大 Survivor 还是该压低阈值
先看连续多次 GC 的 age 分布趋势(用 grep "age" gc.log | head -30):
- 如果
age 1总是占满Desired survivor size甚至翻倍,且total累计值快速逼近老年代可用空间 → 优先扩容新生代或调小SurvivorRatio(例如从 8 改成 6) - 如果
age 1很小,age 2明显突增,之后迅速衰减(比如age 2: 800KB,age 3: 50KB),说明大量对象恰好活过 2 次 GC 才死 → 可考虑把MaxTenuringThreshold主动设为 2,避免无效搬运 - 如果
age分布长期扁平(各龄都有稳定占比),且new threshold在 4~6 波动 → 当前配置已较合理,不建议强行干预
注意:MaxTenuringThreshold 唯一值得调低的场景,是确认这些“卡在 2~3 次 GC”的对象确实不会长期驻留,且老年代有余量承接;否则容易引发老年代碎片或提前 Full GC。
别漏掉 PretenureSizeThreshold 和分配担保的影响
有些对象根本不会出现在 age 表里——它们压根没进过 Survivor 区。比如:
- 显式设置了
-XX:PretenureSizeThreshold=1M,而你创建了 1.2MB 的 byte[],它直接进老年代,不参与年龄统计 - Minor GC 前发现 Survivor 区装不下所有存活对象,JVM 触发“分配担保”,把部分对象(尤其是大块连续存活对象)直接送入老年代,也不会计入
age N
所以如果 age 表里总数据量明显小于你预估的存活对象大小,就得回头检查是否被大对象绕过,或者 Survivor 空间不足导致批量跳转。这时候光盯 age 表会误判。










