在 Java 的世界里,“分代年龄”(Age) 是专门为 JVM 垃圾回收(GC)设计的一个计数器

简单来说,它记录了一个 Java 对象在垃圾回收中活过了多少轮。就像打游戏刷副本一样,对象每在垃圾回收的“清洗”中幸存下来一次,它的分代年龄就会 +1


1. 为什么需要“分代年龄”?

这源于 Java 垃圾回收领域著名的 弱分代假说(Weak Generational Hypothesis)

绝大多数的 Java 对象都是“朝生夕死”的。 比如你在一个方法里 new 了一个局部变量,方法结束了,这个对象就没用了。但也有极少数对象(如 Spring 的 Bean、数据库连接池)会一直存活。

为了高效管理内存,JVM 把堆内存分成了两大区域:

  • 新生代(Young Generation): 存放刚出生、寿命短的对象。
  • 老年代(Old Generation): 存放寿命长、常驻内存的对象。

“分代年龄”就是对象从“新生代”晋升到“老年代”的资格证和进度条。


2. 它的工作流程是怎样的?

我们可以把新生代想象成一个“新手村”,老年代想象成“满级主城”:

  1. 出生(0岁): 绝大多数新对象在新生代的 Eden(伊甸园)区 出生。
  2. 第一次渡劫(1岁): 发生了一次 Minor GC(新生代垃圾回收)。如果这个对象没被回收,它会幸存下来,被移动到 Survivor(幸存者)区,同时它的分代年龄变成 1 岁
  3. 继续熬资历(每活过一轮 +1岁): 以后每发生一次 Minor GC,只要它还在幸存者区折腾且没死掉,它的年龄就会加 1 岁。
  4. 晋升老年代: 当它的年龄达到一定阈值(默认是 15 岁,由 JVM 参数 -XX:MaxTenuringThreshold 决定)时,JVM 就会认为:“这小伙子挺能活,应该是个核心常驻对象。” 于是把它晋升(Promote)到老年代

3. 它存在对象的什么地方?

正如我们在讨论“锁”时提到的,分代年龄存在于每个 Java 对象的 对象头(Object Header)的 Mark Word 里面。

在 HotSpot 虚拟机中,JVM 只给分代年龄分配了 4 个比特位(4 bits) 的空间。

💡 为什么默认上限是 15 岁? 因为 4 个比特位能表示的最大二进制数是 1111,换算成十进制就是 15。所以,你无论怎么调整 JVM 参数,分代年龄的最大值也只能设为 15。

万一这个对象熬到了 15 岁,刚进老年代,程序就再也不用它了怎么办?

这种情况在实际开发中不仅存在,而且非常普遍。这种在老年代中死掉、但还没被清理的对象,在 JVM 中被称为“浮动垃圾”(Floating Garbage)“积压垃圾”。

JVM 早就预料到了这种“看错人”的情况,并准备了一套完整的应对机制。


4. 最终兜底:Full GC(老年代垃圾回收)

老年代虽然被称为“养老院”,但它绝对不是“垃圾填埋场”。

当进入老年代的垃圾越来越多,导致老年代的空间快要被装满时,JVM 就会触发一次长痛的Full GC(或者 Major GC)

  • 做法: 这是一次全堆大扫除。垃圾回收器会把老年代里那些“占着茅坑不拉屎”(已经不再被引用的)假长寿对象全部揪出来,统统清理掉。
  • 代价: Full GC 的成本很高,它通常会触发 STW(Stop-The-World),让整个应用程序短暂停顿。因此,JVM 的调优目标之一就是尽量减少 Full GC 的发生。

5. 现代 GC 的智能“止损”:G1 和 ZGC

传统的垃圾回收器(如 CMS)确实要等老年代满了才被动去扫除。但现代 Java(Java 9 之后的 G1,以及 Java 11/17 引入的 ZGC)聪明得多,它们不再死板地看“分代年龄”。

划分逻辑区域(Region)

现代 GC 把整个堆内存拆成了几百甚至上千个小格子(Regions)。

  • 哪怕一个对象年纪很大,掉进了一个已经全是垃圾的格子(Region)里。
  • G1 垃圾回收器会进行收益评估:“这个格子里 95% 都是垃圾,清理它性价比极高!”
  • 于是,G1 会在不触发 Full GC 的情况下,顺手把这个格子里的“老年代垃圾”给回收掉。

6. 开发者应该反思什么?(避免“过早晋升”)

如果你的代码里频繁出现“到了老年代就不用了”的对象,通常意味着程序出现了“过早晋升”(Premature Promotion)。

常见的原因有两个:

原因一:新手村(Survivor 区)太小了

  • 场景: 发生 Minor GC 时,存活的对象太多,Survivor 区装不下了。
  • 结果: 此时 JVM 会触发担保机制,一些只有 1 岁、2 岁的年轻对象,被迫直接“偷渡”到老年代。
  • 解决办法: 增大堆内存,或者调整参数(如 -XX:SurvivorRatio),让新手村大一点,把他们尽量憋在新生代里杀掉。

原因二:代码里有大对象(比如大数组、巨型字符串)

  • 场景: 你在代码里 new 了一个 10MB 的字节数组,用来做临时文件读取。
  • 结果: 这种大对象,JVM 连新手村都不让进,直接出生在老年代。读取完文件后,它瞬间就成了老年代里的废品。
  • 解决办法: 避免一次性加载海量数据,改用流式处理(Stream),或者复用对象池。

总结

万一对象到了老年代才死,JVM 依然能通过 老年代大扫除(Full GC) 或者 现代 GC 的区域轮询(G1/ZGC) 把它清理掉。

但这是一种“性能损耗”,作为开发者,我们写的代码最好让“朝生夕死”的对象在新生代就被干掉,尽量别去惊动老年代。