JVM 运行时内存与堆分代
更新时间:2026-09-18。本文聚焦「堆为什么要分代、对象在各代之间怎么流动、什么时候进老年代」。运行时数据区的整体概览可先读 JVM 内存模型与对象。
一、运行时数据区:共享还是私有
| 区域 | 线程 | 存什么 | 异常 |
|---|---|---|---|
| 堆 Heap | 共享 | 所有 new 出来的对象、数组 | OutOfMemoryError: Java heap space |
| 方法区/Metaspace | 共享 | 类元数据、常量池、静态变量 | OutOfMemoryError: Metaspace |
| 虚拟机栈 | 私有 | 栈帧:局部变量表、操作数栈、返回地址 | StackOverflowError / OOM |
| 本地方法栈 | 私有 | native 方法的栈帧 | 同上 |
| 程序计数器 PC | 私有 | 当前线程执行的字节码行号 | 唯一不会 OOM 的区域 |
记忆点:栈里放引用和基本类型,堆里放对象;GC 只回收堆(Metaspace 在类卸载时回收)。
二、堆为什么要分代
绝大多数对象「朝生夕死」(方法内的临时对象),少数对象(缓存、连接、Spring 单例)长期存活。把这两类对象分开存放,就能用不同算法:新生代用复制算法(每次只留少量存活对象,复制成本低),老年代用标记-清除/标记-整理(存活多、无需复制)。

新生代内部 Eden : Survivor0 : Survivor1 = 8 : 1 : 1,始终有一块 Survivor 是空的,可用内存是新生代的 90%,浪费仅 10%。
三、对象的一生:从 Eden 到老年代
bash
new 对象
│
▼
Eden(绝大多数对象在此创建)
│ Eden 满,触发 Minor GC:存活对象复制到 S1,年龄 +1
▼
Survivor(在 S0/S1 之间来回复制,每活过一次 Minor GC 年龄 +1)
│ 满足晋升条件
▼
老年代 Old(Major GC / Full GC 回收,停顿长)对象进入老年代的 4 种情况:
- 年龄达到阈值:默认
MaxTenuringThreshold=15(CMS 是 6),在 Survivor 中熬过 15 次 Minor GC 即晋升。 - 大对象直接进老年代:超过
PretenureSizeThreshold的大数组/长字符串直接分配在老年代,避免在 Eden/Survivor 间来回复制(只对 Serial/ParNew 有效,G1 用 Region 视角看待)。 - 动态年龄判断:某次 Minor GC 后,Survivor 中相同年龄所有对象大小总和超过 Survivor 空间一半,年龄大于等于该值的对象直接晋升,不必等到 15。
- 空间分配担保:Minor GC 前若老年代连续空间 < 历次晋升平均大小,会先检查担保设置;不满足且不允许冒险就直接触发一次 Full GC。
四、三种 GC 别再混淆
| 名称 | 回收区域 | 频率 | 停顿 |
|---|---|---|---|
| Minor GC / Young GC | 新生代 | 高、很快 | 短(复制少量存活对象) |
| Major GC | 老年代 | 低 | 较长,常与 Full GC 混用 |
| Full GC | 整堆 + Metaspace | 最低 | 最长,要重点避免 |
五、常用参数
bash
-Xms4g -Xmx4g # 堆初始/最大值;生产设成相同,避免运行时扩缩容抖动
-Xmn1g # 新生代大小(也可用 -XX:NewRatio 设老年代:新生代比例)
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值
-XX:+UseG1GC # 使用 G1(JDK 9+ 默认)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/六、常见坑
-Xms和-Xmx设得不一样:生产环境堆会在运行时动态扩容,扩容瞬间可能触发卡顿;通常两者设成相同值。- 新生代设太小:Minor GC 异常频繁,对象来不及「长大」就被提前晋升,老年代快速膨胀引发 Full GC;设太大又会让单次 Minor GC 停顿变长——要结合存活对象大小和 RT 预算调。
- 看到 Full GC 频繁只调堆大小:可能根本不是容量问题,而是大对象、内存泄漏、显式
System.gc()、Metaspace 不足等,先看 GC 日志分清原因。 - 误以为年龄阈值到了才晋升:动态年龄判断和空间担保会让对象「提前」进老年代,排查老年代增长要把这两条算进去。
G1 收集器如何用 Region 重构这套分代,见 G1 收集器 Region 与回收过程 与 G1 与 CMS 的区别;线上频繁 Full GC 的排查见 FAQ「Full GC 频繁怎么排查」,堆大小经验值见「JVM 堆内存一般设多少」。
总结
JVM 内存分线程共享的堆/方法区与线程私有的栈/PC,堆按「朝生夕死」经验分成新生代(Eden+两块 Survivor,8:1:1,用复制算法)和老年代(用标记-清除/整理);对象先在 Eden 分配、Minor GC 后在 Survivor 间复制并累加年龄,达到阈值、超大、触发动态年龄判断或空间担保时晋升老年代;Minor GC 快而频繁、Full GC 慢而要极力避免,调优先固定 -Xms=-Xmx、再平衡新生代大小,并能区分 Full GC 到底是容量、泄漏还是大对象引起。