分代收集是统计规律倒逼的工程解:90%对象活不过一次Minor GC,99%活过10次GC的会长期存活,JVM据此划分新生代与老年代以提升GC效率。

分代收集不是设计选择,是统计规律倒逼出来的工程解
Java堆划分为新生代和老年代,根本原因不是“听起来合理”,而是大量真实应用跑出来的数据:90%以上对象活不过一次Minor GC,而活过10次GC的对象,99%会一直活到应用结束。JVM只是把这种“朝生夕死+长命百岁”的两极分布,用内存区域固化下来。
如果不分代,每次GC都得扫描整个堆——哪怕你只创建了10个新对象,也要遍历GB级的老年代。分代后,Minor GC只扫Eden + 一个Survivor区,通常不到堆的1/4,停顿时间从几百毫秒压到几毫秒。
新生代为什么必须用复制算法?
因为Eden区对象死亡率常年在95%以上,复制算法在这种场景下效率碾压其他算法:它不标记、不清除、不整理,只把那5%存活对象搬走,剩下的空间直接清零。但这个优势有硬前提——必须保证“能搬进去”。
-
SurvivorRatio设太小(比如-XX:SurvivorRatio=2),Survivor区只有Eden的1/2,很快撑爆,触发提前晋升,老年代迅速膨胀 - 大对象(如大数组)默认绕过Eden,直接进老年代——
-XX:PretenureSizeThreshold没调好,可能让本该短命的临时缓冲区也直奔老年代 - 对象年龄阈值
MaxTenuringThreshold默认15,但多数业务里对象活过6~8次GC就稳定了,调太高反而拖慢晋升,调太低又导致Survivor反复拷贝浪费CPU
老年代回收为什么让人提心吊胆?
因为老年代用的是标记-清除或标记-整理算法,没有复制算法那种“清空即重来”的爽快感。一次Full GC可能卡住应用几百毫秒甚至数秒,尤其当老年代碎片化严重时。
立即学习“Java免费学习笔记(深入)”;
常见诱因不是内存真不够,而是“假性不足”:
- 年轻代GC后大量对象晋升,但老年代剩余空间虽够,却无法容纳连续的大块——这是
Concurrent Mode Failure的典型前兆 -
System.gc()被显式调用,强制触发Full GC,而很多SDK(尤其是老版本Logback、Netty)会在初始化时偷偷调它 - 元空间(Metaspace)耗尽也会连带触发Full GC,但错误日志里只报
java.lang.OutOfMemoryError: Metaspace,容易误判为堆问题
调参不是玄学,关键看三组数字
真正决定分代行为的,就三个指标:对象创建速率、平均晋升大小、老年代GC频率。所有JVM参数调整都该围绕它们验证。
- 用
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps打开日志,重点关注每次Minor GC后Eden释放量和Survivor占用变化——如果Survivor长期>80%,说明SurvivorRatio或MaxTenuringThreshold该动了 - 观察
Full GC前的Old区使用率,如果总在70%~85%触发,大概率是CMS或G1的初始阈值太激进;ZGC/Azul的-XX:SoftRefLRUPolicyMSPerMB之类参数反而影响不大 - 晋升失败(
Promotion Failed)比OOM更危险——它意味着GC已失去调控能力,此时再调堆大小意义有限,得先查是不是有缓存没设上限、或是String.intern滥用
分代机制本身很稳定,真正出问题的,几乎全是对象生命周期被业务逻辑悄悄改写了:比如本该单次请求用完的DTO,被塞进了静态Map;或者以为是临时字符串拼接,结果背后是StringBuilder反复扩容持有引用。内存分区只是镜子,照出来的是代码里的隐式状态泄漏。










