对象年龄是jvm在每次minor gc后直接递增并写入对象头mark word中4位的计数器,范围0–15,仅minor gc更新,晋升老年代后失效;jol工具可验证,动态判定逻辑独立于age字段。

对象年龄在OpenJDK中不是“计算”出来的,而是每次Minor GC后由JVM直接递增并写入对象头的Mark Word里——它是个计数器,不是推导值。
对象年龄存在哪?怎么被读取
年龄字段固定占Mark Word中的4位(bit),位置和解释方式由JVM实现决定,但所有OpenJDK主流版本(8–21)都复用同一块标记空间。它和锁标志位、偏向线程ID等共享这8字节(64位JVM压缩指针下)或16字节(未压缩)的头部区域。
你无法在Java代码里直接访问这个值,但可用jol(Java Object Layout)工具验证:
java -jar jol-cli.jar internals java.lang.Object
输出中会明确标出age: 0字段;若对象经历过一次GC并存活下来,再次运行该命令(在GC发生后)会显示age: 1。
- 年龄从0开始:对象在
Eden区分配时,age初始为0 - 仅Minor GC触发+1:只有年轻代GC(
Minor GC)才会使存活对象的age加1;Full GC不改变年龄 - 晋升后清零或失效:一旦对象进入老年代,
age字段不再更新,也不再参与任何判定逻辑
年龄值为什么最大是15
因为4位二进制能表示的范围是0~15(0b1111 = 15),这是硬编码在HotSpot源码里的限制,与-XX:MaxTenuringThreshold参数无关——后者只是告诉JVM“别让对象活过X次GC才晋升”,但底层存储上限就是15。
当age达到15时,JVM会在下次Minor GC中强制将其晋升到老年代,不管Survivor空间是否充足、动态阈值是否更低。
- 这不是“建议值”,而是溢出保护:如果
age试图写入16,会截断为0,导致逻辑错乱,所以JVM在C++层做了MIN(age + 1, 15)处理 -
-XX:MaxTenuringThreshold=15是默认值,但设成20也没用——实际生效的仍是15 - G1回收器也遵守此约束,尽管它用
Age Table管理Region级年龄分布,底层单个对象的age字段仍是4位
动态年龄判定和age字段的关系
动态年龄判定(如new threshold 2出现在GC日志中)并不修改对象头里的age值,它只是JVM在每次GC后扫描Survivor区时,按当前各年龄档对象总大小做累加判断——这个过程完全在GC线程内存中完成,不碰对象头。
换句话说:age字段只负责记录“这个对象熬过了几次Minor GC”,而“要不要让它现在就走”由另一套独立逻辑决定。
- 动态判定依赖
-XX:+UseAdaptiveSizePolicy开启,否则退化为纯看age是否≥MaxTenuringThreshold - 即使
age还是1,只要同龄对象总和超TargetSurvivorRatio(默认50%),JVM就会在下一轮GC中把所有age ≥ 1的对象全送走 - 所以你可能看到日志里
new threshold 1,但某个对象age字段仍显示1——它这次就被晋升了,没机会变2
真正容易被忽略的是:对象年龄不是性能指标,也不是调优第一入口。盲目调高MaxTenuringThreshold或压低TargetSurvivorRatio,往往掩盖了对象生命周期设计问题——比如本该复用的短命缓存被长期持有,或不该缓存的大对象卡在Survivor里反复复制。盯住GC日志里的age变化趋势,比盯着单个数字更有价值。











