对象年龄计数器由jvm在每次minor gc后自动更新:对象从eden幸存并复制到survivor时年龄置1,此后每幸存一次minor gc年龄加1;若survivor空间不足或触发动态年龄判断(某年龄及以上对象总和超survivor一半),则提前晋升老年代。

对象年龄计数器怎么更新?
对象年龄计数器不是你手动维护的,而是 JVM 在每次 Minor GC 后自动加 1 的——前提是该对象从 Eden 区幸存下来,并成功复制到 Survivor 区(无论 From 还是 To)。只要对象在 Survivor 区之间来回“搬来搬去”,每熬过一次 Minor GC,年龄就 +1。
注意:GC 触发时机、Survivor 空间是否充足、以及 MaxTenuringThreshold 设置都会影响它是否真能累加到预期值。
- 对象第一次从 Eden 复制到 Survivor,年龄变成 1
- 下一次 GC,如果它还在 Survivor 中(被复制到另一个 Survivor),年龄变成 2
- 如果某次 GC 时目标 Survivor 空间不够,JVM 可能直接把它提前晋升到老年代,年龄计数器就“作废”了
-
MaxTenuringThreshold默认是 15,但实际阈值可能更低——取决于动态年龄判断逻辑(见下一条)
对象什么时候会跳过年龄阈值直接进老年代?
JVM 不死守 MaxTenuringThreshold。当某次 Minor GC 后,某个 Survivor 区里,**年龄 ≥ 某个值的所有对象总大小 > Survivor 空间的一半**,JVM 就会把所有年龄 ≥ 这个“临界年龄”的对象一次性晋升——不管它们有没有达到 MaxTenuringThreshold。
这是为了防止 Survivor 区反复碎片化、复制失败。换句话说:年龄只是参考,空间压力才是硬门槛。
立即学习“Java免费学习笔记(深入)”;
- 比如 Survivor 是 2MB,当前存活对象中,所有年龄 ≥ 4 的对象加起来占了 1.1MB → 那所有年龄 ≥ 4 的对象本轮全部晋升
- 这个“临界年龄”是 GC 时动态计算的,不固定,也不暴露给应用层
- 开启
-XX:+PrintGCDetails可在日志里看到类似age 1: 123456 bytes, age 2: 234567 bytes...的统计行
如何验证对象年龄和晋升行为?
靠日志最直接。用 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 启动,再配合一个能稳定触发 Minor GC 的测试(比如循环 new 大量短命对象 + 强制 System.gc() 不推荐,改用分配压力更可靠)。
关键看 GC 日志里 Survivor 区的 “Desired survivor size” 和各年龄档的字节数分布。如果发现某次 GC 后,tenured 区增长明显,且对应 Survivor 日志里高龄段字节占比突增,基本就是动态晋升生效了。
- 不要依赖
java.lang.ref.Reference或Instrumentation.getObjectSize()查年龄——它们查不到 -
jstat -gc <pid></pid>能看 S0/S1 使用率和 YGC 次数,辅助判断是否频繁 GC 导致年龄累积快 - 用
-XX:MaxTenuringThreshold=1强制“一岁就走”,适合调试,但线上慎用——可能引发老年代过早膨胀
常见误判和性能隐患
很多人以为“对象活过 15 次 GC 才进老年代”,结果压测时老年代涨得飞快,怀疑代码有内存泄漏。其实更可能是 Survivor 空间太小,或者对象平均生命周期偏长,触发了动态晋升,让一批年龄才 3~5 的对象集体“插队”。
另一个坑是调大 SurvivorRatio(比如设成 8)却不调 MaxTenuringThreshold,导致 Survivor 变大后,同样一批对象需要更多轮 GC 才能凑够“一半空间”,反而延长了它们在年轻代驻留时间,增加 Minor GC 压力。
- 调整
SurvivorRatio时,务必同步观察 GC 日志中的年龄分布,而不是只盯YGC次数 - 使用 G1 或 ZGC 时,这套年龄机制不适用——它们用的是区域复制 + 引用关系分析,没有传统意义上的 Survivor 区和年龄计数器
- 对象是否被晋升,最终取决于 GC 算法决策,不是对象自己“申请”的——别在代码里试图“控制年龄”










