对象年龄是对象在survivor区中每经历一次minor gc存活后加1的隐式计数器,新对象在eden区年龄为0,首次幸存进入survivor区变为1,后续每次在survivor间复制存活均+1;其晋升老年代不仅取决于默认阈值15,更受动态年龄判定(累计同龄对象超survivor一半即晋升)、大对象直入、分配担保等机制影响。

Java 垃圾回收中,对象从 Eden 区晋升到老年代的“年龄计数器”(Age)并不是一个独立存储的变量,而是对象头中用于记录 GC 次数的隐式标记。它本质是对象在 Survivor 区中“幸存”的次数,而非在 Eden 中停留的时间。
年龄计数器怎么算出来的
每次 Minor GC 时,JVM 会扫描 Eden 和其中一个 Survivor(From)区中的存活对象。这些对象被复制到另一个 Survivor(To)区,并且每成功经历一次 Minor GC 仍存活,其年龄就加 1。
- 新分配在 Eden 的对象,年龄为 0
- 第一次 Minor GC 后,从 Eden 或 From 幸存并进入 To 区的对象,年龄变为 1
- 下一次 GC,该对象若仍在 To 区存活,会被复制回 From 区(Survivor 空间交换),年龄变为 2
- 年龄只在对象被复制到 Survivor 区时递增,Eden 中的对象不会直接增加年龄
什么情况下对象会提前进入老年代
年龄达到阈值(默认 15)只是晋升条件之一,JVM 还会根据空间压力动态调整。以下情况会导致对象绕过年龄检查,直接晋升:
- Survivor 区放不下当前批次的存活对象:即使年龄还小,也会直接进入老年代(称为“担保失败”或“分配担保”)
- 大对象直接分配到老年代:通过 -XX:PretenureSizeThreshold 设置阈值,超过该大小的对象跳过年轻代,直接进老年代(不参与年龄计数)
- 动态年龄判定:如果某次 Minor GC 后,Survivor 中所有年龄 ≥ n 的对象总大小 > Survivor 空间的一半,则年龄 ≥ n 的对象全部晋升——这个 n 可能远小于 15
年龄阈值不是固定死的
-XX:MaxTenuringThreshold 参数可设置最大年龄阈值(范围 0~15),但实际生效值受 JVM 自动调整影响:
- 设为 0:表示对象经过一次 GC 就晋升(即不经过 Survivor,类似“无年龄”策略)
- 设为 1:对象在 Survivor 存活一次后就晋升
- 默认值 15 是 HotSpot 的上限,但多数场景下因 Survivor 空间不足或动态判定,对象往往在年龄 2~6 就已晋升
- JVM 在运行时可能将实际阈值调低(如降到 4),以避免 Survivor 区碎片化或频繁复制开销
如何观察年龄分布和晋升行为
启用 GC 日志可验证年龄流转逻辑:
- 添加参数:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
- 日志中会出现类似
Desired survivor size 1048576 bytes, new threshold 7 (max 15),说明本次 GC 后 JVM 动态设定了晋升阈值为 7 - 使用 -XX:+PrintTenuringDistribution 可打印每次 GC 后各年龄对象的大小分布,直观看到哪些年龄的对象被晋升
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











