堆内存分年轻代和老年代是因为对象生命周期差异大:95%对象短命,少数长期存活;不分代会导致每次GC扫描全堆、效率极低;分代后年轻代用复制算法(快且无碎片),老年代用标记-整理或清除算法(兼顾空间与吞吐)。

堆内存为什么分年轻代和老年代
因为对象生命周期差异极大:95%以上对象朝生暮死,少数对象长期存活。不分代的话,每次 GC 都要扫描全部堆,效率极低。分代后,JVM 可以对年轻代用复制算法(快、无碎片),对老年代用标记-整理或标记-清除(兼顾空间利用率和吞吐)。这不是设计偏好,而是性能刚需。
- 年轻代默认占堆总大小的
1/3(可通过-XX:NewRatio调整),老年代占2/3 - 年轻代中
Eden:S0:S1 = 8:1:1是 HotSpot 默认比例,可通过-XX:SurvivorRatio=8显式设置 - 对象不是“出生即在 Eden”——大对象(如大数组)可能直接分配到老年代,由
-XX:PretenureSizeThreshold控制阈值
Eden 区触发 Minor GC 的真实条件
不是“Eden 满了才回收”,而是“Eden + 一个 Survivor 区放不下新对象时”触发。Minor GC 后,Eden 和“From Survivor”中存活的对象会被复制到“To Survivor”,年龄+1;若 To 空间不足,则超过阈值(默认 15)或大对象直接晋升老年代。
- 年龄阈值由
-XX:MaxTenuringThreshold控制,默认 15,但实际晋升可能早于该值(如 Survivor 空间不够) - 每次 Minor GC 后,两个 Survivor 区角色互换(S0 ↔ S1),始终保证一个为空
-
java.lang.OutOfMemoryError: Java heap space常见于 Eden 区频繁 GC 但对象持续无法释放——此时要看是不是内存泄漏,而非简单调大堆
老年代触发 Full GC 的典型场景
Full GC(即 Major GC / Old GC)代价极高,会 Stop-The-World(STW),所有应用线程暂停。它不只在老年代满时发生:
- 年轻代对象晋升时老年代空间不足(
promotion failed) - 元空间(
Metaspace)扩容失败(注意:不是方法区溢出,1.8+ 已无永久代) - 显式调用
System.gc()(除非加了-XX:+DisableExplicitGC,否则仍可能触发) - 堆内存使用率长期 >90%,CMS 收集器会提前启动并发模式失败后的兜底 Full GC
注意:OutOfMemoryError: GC overhead limit exceeded 表示 GC 花费超 98% 时间却只回收不到 2% 堆内存——这是 JVM 自动抛出的保护性错误,说明堆已严重碎片化或对象泄漏,不是参数调优能解决的。
如何验证当前堆结构与对象分布
别猜,用工具看。最轻量的是 JVM 自带的 jstat,配合 GC 日志更准:
jstat -gc1000 5 # 每秒打印一次 GC 统计,共5次 # 输出关键列:S0C/S1C(Survivor 容量)、EC(Eden 容量)、OC(Old 容量)、YGC(年轻代GC次数)、YGCT(年轻代GC耗时)、FGC(Full GC次数)
- 加
-Xlog:gc*:file=gc.log:time,tags,level(JDK 11+)或-XX:+PrintGCDetails -Xloggc:gc.log(旧版)开启详细 GC 日志 - 用
jmap -histo查看堆中对象数量与大小排名,快速定位疑似泄漏类型 - 避免在生产环境随意用
jmap -dump,可能引发长时间 STW;优先用jcmd辅助判断是否元空间或直接内存问题VM.native_memory summary
堆结构不是静态配置表,它是动态博弈的结果:对象创建速率、晋升节奏、GC 器策略、系统负载共同决定每毫秒的内存布局。调参前先观测,观测前先理解“谁在哪儿、活多久、为什么留不下来”。










