java垃圾回收什么时候从新生代到老年代,什么时候从老年代移除
一、什么时候从新生代到老年代
Java 对象从新生代(Young Generation)晋升到老年代(Old Generation),主要遵循以下几个核心规则。理解这些机制对于 JVM 调优和排查 Full GC 问题至关重要。
1. 长期存活的对象(年龄阈值)
这是最经典的晋升机制。
- 原理:每个对象在经历一次 Minor GC 后,如果仍存活,其“年龄”(Age)就加 1。
- 阈值:当对象的年龄超过设定的阈值(
-XX:MaxTenuringThreshold,默认值为15,CMS 收集器下为6)时,就会被移动到老年代。 - 本质:这是一个“熬过多次收集”的过程,用于保留那些生命周期较长的对象。
2. 大对象直接进入老年代
为了避免大对象在 Eden 区和 Survivor 区之间频繁复制,降低 GC 开销。
- 判定标准:对象大小超过
-XX:PretenureSizeThreshold参数设定的值(该参数只对 Serial 和 ParNew 收集器有效)。 - 典型场景:很长的字符串、大型数组(如大尺寸的
byte[]缓存)。 - 注意:即使这个对象在新生代是“朝生夕死”的,也会直接分配在老年代。
3. 动态对象年龄判定(Survivor 区自适应)
JVM 并不总是要求对象必须达到MaxTenuringThreshold才能晋升。
- 规则:如果在 Survivor 空间中,相同年龄所有对象大小的总和> Survivor 空间的一半(通常是 50%),那么年龄大于或等于该年龄的对象就可以直接进入老年代。
- 目的:这是一种动态保护机制,防止 Survivor 区因某些年龄段对象过多而被填满,导致 Minor GC 效率下降。
4. 空间分配担保(Minor GC 前的兜底)
这是在发生 Minor GC 之前的一个流程判断,间接导致对象进入老年代。
- 流程:
- 在发生 Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总空间。
- 如果大于,Minor GC 确保是安全的。
- 如果小于,则查看
-XX:HandlePromotionFailure参数是否允许担保失败。
- 如果允许,JVM 会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。
- 如果大于,将尝试进行一次有风险的 Minor GC。
- 如果小于,或者不允许担保失败,则会触发一次Full GC。
- 在发生 Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总空间。
- 结果:在这次 Minor GC 中,如果 Survivor 区无法容纳存活的对象,这些对象会通过分配担保机制直接进入老年代。
5. 总结与对比表
| 晋升场景 | 触发条件 | 核心目的 |
|---|---|---|
| 年龄达标 | 对象年龄 >MaxTenuringThreshold | 保留长生命周期对象 |
| 大对象 | 对象大小 >PretenureSizeThreshold | 减少复制开销,防止过早占满 Survivor |
| 动态年龄 | Survivor 中同年龄对象总和 > Survivor 的一半 | 防止 Survivor 溢出,动态优化 |
| 分配担保 | Minor GC 后 Survivor 空间不足 | 兜底机制,确保 GC 能正常完成 |
6. 常见相关问题与调优建议
- 过早晋升(Premature Promotion):对象本应在新生代回收,却过早进入老年代,导致老年代增长过快,引发频繁 Full GC。通常是因为 Survivor 区太小或
MaxTenuringThreshold设置过低。 - 晋升失败(Promotion Failed):在进行 Minor GC 时,老年代没有足够的连续空间容纳要晋升的对象。这通常意味着老年代已满或碎片化严重,需要触发 Full GC。
- 调优思路:
- 合理设置
-Xmn(新生代大小),避免过小。 - 关注 Survivor 区的使用率(
jstat -gcutil)。 - 对于短命的大对象(如临时处理大文件),应尽量避免,否则它们会直接撑满老年代。
- 合理设置
简单来说:要么“活得久”,要么“长得大”,要么“Survivor 装不下”,对象就会去往老年代。
对象不会因为"在老年代待久了"就被回收;它得先变成垃圾,再等到一次能扫老年代的 GC 发生。老年代对象在 Old GC / Major GC / Full GC 里才可能被清;从 Old Gen 消失,通常就意味着发生了一次 major/full GC,而且只有到 GC Roots 不可达的 dead object 才会被清——如果老年代里全是活对象还塞满了,JVM 不是清活对象,而是抛OutOfMemoryError。
二、什么时候从老年代移除
1. 先决条件:它得先"没人引用了"
不管哪种 GC,第一步都是可达性分析:从 GC Roots(活跃线程栈局部变量、static 变量、JNI 引用、synchronized 持有对象等)往下追,追得到的叫存活,追不到的才是垃圾。
所以一个进老年代的对象,只要还被下面这些地方握着,它就是"老不死":
staticMap<Long,BigObject>CACHE=newConcurrentHashMap<>();// BigObject 进了老年代,但只要 CACHE 还引用它,GC 不敢动只有后来把引用链断掉:
CACHE.remove(key);obj=null;它才在这个意义上"已经是垃圾"——但还没真消失,只是等着 GC 来扫。换句话说:断开引用是把"死刑判决"写好,老年代 GC 才是"执行枪决"。
2. 经典分代 GC:Serial / Parallel / CMS 时代
如果用的是传统分代模型(Serial Old、Parallel Old、CMS 这套),老年代对象在这些时机被清:
Old GC / Major GC:专门扫老年代
当老年代空间不足、CMS 达到-XX:CMSInitiatingOccupancyFraction阈值(常见默认 92%)时,会触发 Old/Major GC,把老年代里不可达对象清掉。很多资料把只回收老年代叫 Old GC / Major GC,把整堆 + 元空间那种叫 Full GC;但"Major/Full"这俩词在生产语境里经常被混用,Oracle/微软文档也提醒过这些术语 JVM 规范里并不严格。
Full GC:整堆一起清
更常见、也更疼的是 Full GC——新生代 + 老年代 + 元空间一起扫。触发点一大把:
- 老年代满了
- Minor GC 前发现老年代腾不出空间给晋升对象
- 元空间 Metaspace 满了
- 手贱
System.gc() - CMS 并发失败、晋升失败 promotion failed
Full GC 才是真正能把老年代里大量垃圾一次性清出来的场景,代价是 STW 最长,应用会明显卡一下。
都活着还塞满?直接 OOM
老年代满了 ≠ JVM 会把"活对象"踢出去。GC 只清 dead object。真要是老年代全被有效引用占满、Full GC 后还分配不出空间,JVM 就抛OutOfMemoryError: Java heap space。Stack Overflow 那个经典问答说得很直白——只有不可达的才移走,全活着就 OOM。
3. G1 GC:不是"老年代满才清",是 Mixed GC
到了 G1(现在 JDK 默认 GC),模型变了——它把堆切成一个个 Region,Old 是"逻辑上的老年代 Region"。所以 G1 里"从老年代移除"是这样的:
- 普通 Young GC:对象照样往 Old Region 晋升
- 老年代占用到 IHOP 阈值(默认约 45%),G1 启动并发标记
- 标记完进入 Space-Reclamation / Mixed GC 阶段:不只清 Young Region,还会挑一批"垃圾多、收益高"的 Old Region 一起清
- 实在来不及:老年代涨太快、Mixed GC 跟不上,G1 也会退化成更重的 Full GC
Oracle G1 文档自己写的:G1 有 Young-Only 阶段慢慢往 old 填对象;等 old 到 Initiating Heap Occupancy threshold 就进并发标记;后面 Space-Reclamation 阶段才增量回收 old generation regions。所以 G1 里老年代垃圾是"提前、分批、按收益"清的,不是非等满。
4. ZGC / Shenandoah
到了 ZGC、Shenandoah 这种现代低延迟 GC,故事又翻了一层:它们主打并发标记、并发转移/压缩、亚毫秒到很低 STW,很多实现弱化了传统 old/young 的痛点,或用分代 ZGC 的年轻代逻辑,但整体不再靠传统 Full GC 救场。
也就是说——真用 ZGC/Shenandoah,它是在并发回收周期里、对象不可达时被并发/低停顿地搬走/回收掉的。Shenandoah/ZGC 资料也常把它们描述为不再或不再强烈区分传统分代、靠并发压缩/染色指针搞极低停顿。
5. 生产里最常见的"清不掉"坑
下面这几种引用链,会让对象明明"业务逻辑不用了",却还被 GC Roots 拽着,于是永远滞留在老年代,最后把老年代顶爆:
staticList<Object>LIST=newArrayList<>();// 静态集合忘了 remove/clean → 对象一直被 Root 引用 → 老年代也清不走ThreadLocal<byte[]>tl=newThreadLocals();tl.set(newbyte[1*1024*1024]);// 线程池线程复用,ThreadLocal 没 remove → value 跟着线程活 → 老年代 leak所以看到的现象往往是这个链条:老年代慢慢涨 → 涨到阈值 → 触发 Old/Full GC → 真垃圾被清掉、空间回落;但如果是内存泄漏,FGC 也清不动 → 最后 OOM。这也是为啥 Full GC 后堆还很高,第一反应就该怀疑 static map / 缓存没淘汰 / ThreadLocal 泄漏 / 长生命周期集合无限增长。
6. 总结
对象进老年代后,只要还被 GC Roots 引用,就一直待着;等 Old GC / Major GC / Full GC / G1 Mixed GC / ZGC 并发回收 发生时,从 GC Roots 不可达的老对象才被清掉;如果老年代满且全是活对象 → OOM。
- 进老年代:活得久、长得大、Survivor 装不下、分配担保
- 出/清老年代:不可达 + 发生能扫老年代的 GC
- 清不掉:还被 static / cache / 线程池 ThreadLocal / 活跃线程引用
- 最坏:Full GC 后还活不满 →
OutOfMemoryError
