复制算法的核心是切换指针而非搬数据;s0和s1成对存在以确保每次minor gc都有空闲“to”区接收存活对象,避免覆盖与碎片,二者大小相等、角色动态轮转,年龄按跨survivor复制次数累加,受动态判定与担保机制影响晋升。

复制算法的核心不是“搬数据”,而是“换指针”。S0 和 S1 的切换,本质是为每次 Minor GC 快速腾出一块干净、连续、无碎片的接收区,让存活对象能一口气复制过去,不扫描、不整理、不覆盖自己。
为什么必须有两个 Survivor 区
单个 Survivor 区无法支撑安全复制。假设只有 S0:Eden 满了触发 GC,要把 Eden 里活的对象和 S0 里还活着的对象一起搬进去——但 S0 自己正被使用,没地方放;硬往里塞,就会覆盖正在读取的对象,引发数据错乱。S0/S1 成对存在,就是提供一个始终空闲的“To”区,确保复制有确定落点。
- 二者大小严格相等,由 -XX:SurvivorRatio 控制(如设为 8,表示 Eden:S0:S1 = 8:1:1)
- 不能只禁用 S0 或只调大 S1;JVM 启动即固定结构,破坏对称性会导致 GC 异常或对象提前晋升
- 没有 S0/S1 轮转,Minor GC 就退化成标记-清除,必然产生碎片,长期运行后分配失败风险陡增
S0 和 S1 是怎么“交换”的
交换不移动内存,也不重命名。它只是 GC 线程在复制任务末尾更新一次内部指针:把当前空的那个 Survivor 区标记为下一轮的“To”区。比如本轮 GC 把 Eden+S0 的存活对象全复制到 S1,复制完立刻清空 Eden 和 S0,此时 S1 变成“已用区”,S0 变成“空闲区”——下一轮 GC 时,S0 就自动成为新的“To”区。
- GC 日志中看到 S0 used 从 0% 跳到 45%,S1 used 从 45% 降到 0%,这就是翻转发生的直接证据
- 名字 S0/S1 是静态标识,角色 From/To 是动态的;日志里 “PSYoungGen” 段的 before/after 对比可清晰验证
- 翻转动作毫秒级完成,真正耗时的是复制存活对象本身,不是切换逻辑
对象年龄与 S0/S1 复制的关系
对象年龄只在跨 Survivor 区复制时加 1。从 Eden → S0 是第 1 次,S0 → S1 是第 2 次,S1 → S0 是第 3 次……它不看时间,只看“被复制进 Survivor 区的次数”。
- 年龄默认达 15 才晋升,但实际常更早:若某次 GC 后,S1 中所有 age=3 的对象总大小超过 S0 容量的一半,所有 age≥3 的对象立即进老年代(动态年龄判定)
- Survivor 空间不足时,JVM 不等年龄达标,直接把溢出部分对象担保分配到老年代(HandlePromotionFailure 机制)
- 把 -XX:MaxTenuringThreshold 设为 0,等于关闭 Survivor 缓冲,所有存活对象下轮就进老年代,极易触发 Full GC
常见配置误区与影响
调参不是越大越好,关键要匹配对象生命周期分布。Survivor 区过小,对象频繁跳级;过大,则复制成本上升,停顿变长。
- -XX:SurvivorRatio=32:S0/S1 各仅占年轻代约 1.5%,小对象也容易因空间不足直送老年代
- -XX:-HandlePromotionFailure:关闭担保机制后,Survivor 溢出会直接导致 Minor GC 失败,抛 OOM
- 大对象(>-XX:PretenureSizeThreshold)绕过整个 Survivor,直接进老年代,不参与年龄计数










