新生代和老年代的 GC 算法有哪些?
结论先行:新生代用复制算法(Eden + 两个 Survivor,默认 8:1:1),因为对象朝生夕死、存活率低,复制成本小且天然无碎片;老年代用标记-清除(CMS)或标记-整理(Serial Old、Parallel Old),因为存活率高,复制不划算且必须控制碎片。G1 之后不再严格分代,而是按 Region 动态选择「复制 + 整理」的混合回收。
一、两代算法对照
| 维度 | 新生代 | 老年代 |
|---|---|---|
| 算法 | 复制(复制到 Survivor) | 标记-清除(CMS)/ 标记-整理(Serial Old、Parallel Old) |
| 对象特征 | 存活率低(< 10%) | 存活率高、对象大、寿命长 |
| 空间划分 | Eden、S0、S1 | 一整块(或 G1 的 Region) |
| 触发时机 | Eden 满 | 老年代占用达阈值、晋升失败、空间分配担保失败 |
| 停顿 | 短(几十毫秒以内) | 长(可达秒级) |
| 关键参数 | -XX:SurvivorRatio、-XX:MaxTenuringThreshold | -XX:CMSInitiatingOccupancyFraction、-XX:+UseCMSCompactAtFullCollection |
二、新生代的对象流转
new 对象 → Eden(TLAB 优先)
│ Eden 满
▼
Young GC:Eden + From Survivor 的存活对象 → 复制到 To Survivor
│ 每次存活年龄 +1
├─ 年龄 ≥ MaxTenuringThreshold(默认 15)
├─ 同龄对象总和 > To Survivor 的一半(动态年龄判定)
└─ 大对象直接进老年代(-XX:PretenureSizeThreshold)
▼
晋升老年代 → 老年代满触发 Full GC
三、复制算法为什么只浪费 10%
HotSpot 的默认比例是 Eden : S0 : S1 = 8 : 1 : 1,新生代总容量 10 份,每次只用一个 Survivor,另一份是空的「To 区」。
| 参数 | 含义 | 默认 |
|---|---|---|
-XX:SurvivorRatio=8 | Eden 与单个 Survivor 的比值 | 8 |
-XX:MaxTenuringThreshold=15 | 晋升年龄上限 | 15 |
-XX:PretenureSizeThreshold | 超过该大小的对象直接进老年代 | 0(不限制,仅 Serial/ParNew 有效) |
-XX:TargetSurvivorRatio=90 | To 区期望使用率,影响动态年龄判定 | 90 |
存活对象超过 To 区容量时走空间分配担保:先检查老年代最大连续可用空间是否足够,不够则提前触发 Full GC,这就是「担保失败」的由来。
四、用参数观察晋升行为
# 打印每次 Young GC 后各年龄段对象的分布,判断是否过早晋升
java -Xms1g -Xmx1g -Xmn400m \
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=10 \
-Xlog:gc+age=trace -jar app.jar
# 关掉动态年龄判定,排除「同龄对象过半提前晋升」的干扰
java -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -jar app.jar
gc+age=trace 输出形如 Desired survivor size 20971520 bytes, new threshold 10 (max 15) 加各年龄段的字节数,看到大量对象年龄很小就进入老年代,说明 Survivor 太小或对象存活时间超过预期。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么老年代不用复制算法 | 存活率高时复制的对象量接近全量,还要额外一半空间,代价过高 |
| 为什么新生代要用两个 Survivor | 保证复制期间有一块完整空闲区,同时让对象在两次 GC 之间有机会「熬」到老年代 |
| 动态年龄判定的规则 | 某年龄及更小年龄的对象总和超过 To 区一半,就把晋升阈值降到该年龄 |
| 大对象为什么直接进老年代 | 避免在 Eden 与 Survivor 之间反复复制大块内存,-XX:PretenureSizeThreshold 可调 |
| G1 还分新生代老年代吗 | 逻辑上分(Young GC / Mixed GC),物理上是 Region,没有连续的两代空间 |
六、晋升失败与担保机制
| 场景 | 触发条件 | 结果 |
|---|---|---|
| 空间分配担保 | Young GC 前发现老年代连续可用空间不足 | 先做一次 Full GC 腾空间 |
| 晋升失败(promotion failed) | 存活对象要进老年代,但没有连续空间 | 触发 Full GC,通常伴随长停顿 |
| 大对象分配失败 | Humongous 对象找不到连续 Region | G1 退化为 Full GC |
这就是「老年代碎片会让 Young GC 变成 Full GC」的机制来源:担保检查不通过时,一次本该 20ms 的 Young GC 可能直接变成 2s 的 Full GC。
小结
新生代复制、老年代清除或整理,是按存活率做出的经济选择;复制算法靠 8:1:1 把空间浪费控制在 10%,代价是必须处理晋升与担保。答题时把「存活率」作为解释主轴,逻辑就顺了。