一、什么时候从新生代到老年代Java 对象从新生代Young Generation晋升到老年代Old Generation主要遵循以下几个核心规则。理解这些机制对于 JVM 调优和排查 Full GC 问题至关重要。1. 长期存活的对象年龄阈值这是最经典的晋升机制。原理每个对象在经历一次 Minor GC 后如果仍存活其“年龄”Age就加 1。阈值当对象的年龄超过设定的阈值-XX:MaxTenuringThreshold默认值为15CMS 收集器下为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 中如果 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 持有对象等往下追追得到的叫存活追不到的才是垃圾。所以一个进老年代的对象只要还被下面这些地方握着它就是老不死staticMapLong,BigObjectCACHEnewConcurrentHashMap();// BigObject 进了老年代但只要 CACHE 还引用它GC 不敢动只有后来把引用链断掉CACHE.remove(key);objnull;它才在这个意义上已经是垃圾——但还没真消失只是等着 GC 来扫。换句话说断开引用是把死刑判决写好老年代 GC 才是执行枪决。2. 经典分代 GCSerial / 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 failedFull 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模型变了——它把堆切成一个个 RegionOld 是逻辑上的老年代 Region。所以 G1 里从老年代移除是这样的普通 Young GC对象照样往 Old Region 晋升老年代占用到 IHOP 阈值默认约 45%G1 启动并发标记标记完进入 Space-Reclamation / Mixed GC 阶段不只清 Young Region还会挑一批垃圾多、收益高的 Old Region 一起清实在来不及老年代涨太快、Mixed GC 跟不上G1 也会退化成更重的 Full GCOracle 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 拽着于是永远滞留在老年代最后把老年代顶爆staticListObjectLISTnewArrayList();// 静态集合忘了 remove/clean → 对象一直被 Root 引用 → 老年代也清不走ThreadLocalbyte[]tlnewThreadLocals();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