在 Java 的世界里,“分代年龄”(Age) 是专门为 JVM 垃圾回收(GC)设计的一个计数器。
简单来说,它记录了一个 Java 对象在垃圾回收中活过了多少轮。就像打游戏刷副本一样,对象每在垃圾回收的“清洗”中幸存下来一次,它的分代年龄就会 +1。
1. 为什么需要“分代年龄”?
这源于 Java 垃圾回收领域著名的 弱分代假说(Weak Generational Hypothesis):
绝大多数的 Java 对象都是“朝生夕死”的。 比如你在一个方法里 new 了一个局部变量,方法结束了,这个对象就没用了。但也有极少数对象(如 Spring 的 Bean、数据库连接池)会一直存活。
为了高效管理内存,JVM 把堆内存分成了两大区域:
- 新生代(Young Generation): 存放刚出生、寿命短的对象。
- 老年代(Old Generation): 存放寿命长、常驻内存的对象。
“分代年龄”就是对象从“新生代”晋升到“老年代”的资格证和进度条。
2. 它的工作流程是怎样的?
我们可以把新生代想象成一个“新手村”,老年代想象成“满级主城”:
- 出生(0岁): 绝大多数新对象在新生代的 Eden(伊甸园)区 出生。
- 第一次渡劫(1岁): 发生了一次 Minor GC(新生代垃圾回收)。如果这个对象没被回收,它会幸存下来,被移动到 Survivor(幸存者)区,同时它的分代年龄变成 1 岁。
- 继续熬资历(每活过一轮 +1岁): 以后每发生一次 Minor GC,只要它还在幸存者区折腾且没死掉,它的年龄就会加 1 岁。
- 晋升老年代: 当它的年龄达到一定阈值(默认是 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) 把它清理掉。
但这是一种“性能损耗”,作为开发者,我们写的代码最好让“朝生夕死”的对象在新生代就被干掉,尽量别去惊动老年代。