双 survivor 区(s0/s1)为解决标记-复制算法在频繁 gc 下的内存碎片与重复拷贝问题,通过角色轮换实现高效存活对象转移;仅一个 survivor 会导致反复搬运、易撑爆;默认 eden:s0:s1=8:1:1,survivor 过小引发早晋升、老年代压力大,过大则增加复制开销、延长 gc 停顿;g1/zgc 等新收集器已无固定 s0/s1 结构。

为什么新生代要设两个 Survivor 区(S0 和 S1)
不是为了多存点对象,而是为了解决标记-复制算法在频繁 GC 场景下的内存碎片和对象反复拷贝问题。只用一个 Survivor 区的话,每次 Minor GC 都得把所有存活对象从 Eden 搬过去,下次 GC 又得全搬回来——等于白干,还容易撑爆。
双 Survivor 的核心逻辑是“角色轮换”:某次 GC 后,S0 是 From 区(源),S1 是 To 区(目标);下一次就反过来。这样每次只用一个空区接收存活对象,另一个区清空,天然避免覆盖和混乱。
常见错误现象:java.lang.OutOfMemoryError: GC overhead limit exceeded 或 ParNewGC 频繁且耗时飙升——往往是因为 Survivor 空间太小,导致对象“刚活过一轮就晋升”,老年代压力骤增。
-
-XX:SurvivorRatio=8表示 Eden : S0 : S1 = 8 : 1 : 1(默认值),调大该值会压缩 Survivor,容易触发早晋升 - 对象年龄计数器只在 Survivor 区之间复制时+1,Eden → Survivor 算第 1 次,之后每跨一次 S0↔S1 就+1
- 如果 Survivor 空间不足以容纳某次 GC 后的全部存活对象,多余部分会直接“担保分配”进老年代,不等年龄达标
对象什么时候从新生代晋升到老年代
晋升不是只看年龄。JVM 会综合空间压力、对象大小、年龄阈值多个条件动态决定,其中“年龄阈值”只是默认策略之一,实际常被绕过。
立即学习“Java免费学习笔记(深入)”;
典型触发路径有三种:
- 对象在 Survivor 区中“熬过”
-XX:MaxTenuringThreshold次复制(默认 15,但 CMS 下默认 6,G1 不使用该参数) - 某次 Minor GC 后,
S0中相同年龄的所有对象总大小 >S1空间的一半,那所有 ≥ 该年龄的对象直接晋升(动态年龄判定) - 单个对象太大,无法放入 Eden(比如 > 1/2 Eden),直接分配到老年代(
PretenureSizeThreshold控制,仅适用于 Serial / Parallel 收集器)
注意:-XX:+HandlePromotionFailure(JDK 7u4 以后默认开启)决定当 Survivor 不够用时是否启用担保机制——关掉它会导致 Minor GC 失败并抛 OutOfMemoryError。
S0/S1 区大小设置不当的典型后果
很多人以为调大 Survivor 就能减少晋升,结果反而让 GC 更慢、停顿更长。关键在于:Survivor 不是越大越好,而是要匹配应用的对象生命周期分布。
常见症状与归因:
- Minor GC 后老年代占用缓慢但持续上涨 → Survivor 太小,大量对象未达年龄就提前晋升
- 每次 Minor GC 耗时明显变长,且
S0和S1使用率长期接近 100% → Survivor 太大,复制成本高,且可能把本该死掉的对象“养”久了 -
Desired survivor size日志显示远小于实际可用 Survivor 空间 → JVM 认为当前年龄对象太多,主动降低晋升阈值,说明对象存活率偏高
建议用 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 观察每次 GC 后 age 分布,重点关注 Desired survivor size 和实际晋升量。不要凭空调参。
G1 / ZGC 等新收集器里还有 S0/S1 吗
没有。G1 的新生代是逻辑概念,由一组不连续的 Region 动态组成,没有固定命名的 S0/S1;ZGC 和 Shenandoah 根本不区分 Survivor,靠读屏障+并发转移实现对象存活判断。
这意味着:如果你在用 G1,-XX:SurvivorRatio 和 -XX:MaxTenuringThreshold 这些参数已失效(JVM 会忽略或仅作兼容提示)。强行配置不会报错,但也不会起作用。
容易被忽略的一点:很多线上故障排查仍沿用 Parallel GC 时代的思维去分析 G1 日志,看到 GC pause (G1 Evacuation Pause) 还去找 S0 使用率——其实那里的 “survivor” 字样只是日志模板残留,不代表真实结构。










