s0/s1交换是为支持复制算法安全无碎片运行:每次minor gc将eden和from区存活对象一次性复制到空的to区,清空原区;s0与s1大小相等、角色动态翻转,仅靠指针重定向切换,数据不动。

Survivor 区 S0/S1 交换到底在做什么
它不是为了“多存对象”,而是让复制算法能安全、无碎片地运行。每次 Minor GC,JVM 只把 Eden + 当前 From 区(比如 S0)里的存活对象,**一次性复制到空的 To 区(比如 S1)**;复制完立刻清空 From 和 Eden,不扫描、不标记、不整理——这就是快的原因。
关键点在于:S0 和 S1 大小必须相等,且角色由 GC 线程动态翻转,仅靠指针重定向实现切换,数据本身不动。如果只配一个 Survivor,或 -XX:SurvivorRatio 设成非对称值(如 8:2:0),JVM 会直接拒绝启动或触发异常晋升。
对象年龄怎么算、什么时候加 1
年龄计数器只在对象**跨 Survivor 区复制时**才 +1,Eden → S0 是第 1 次,S0 → S1 是第 2 次,S1 → S0 是第 3 次……以此类推。它不看时间、不看创建时刻,只看“被复制到 Survivor 区的次数”。
-
-XX:MaxTenuringThreshold默认是 15,但实际晋升往往早于这个值 - 对象从
Eden直接进S0,年龄就是 1;下次 GC 若还活,进S1,年龄变成 2 - 如果某次 GC 后
S0里所有 age=3 的对象总大小 >S1容量的一半,那所有 age ≥ 3 的对象全进老年代——这叫动态年龄判定,不等你数到 15
为什么 Survivor 不够用时对象会“跳级”进老年代
Minor GC 时,JVM 先估算本轮存活对象总量,再检查目标 Survivor 区(To)是否够装。不够?多余部分不等年龄达标,直接走担保分配(HandlePromotionFailure)进老年代。
这个机制默认开启(JDK 7u4+),但容易被忽略的是:一旦关掉 -XX:-HandlePromotionFailure,Survivor 溢出会导致 Minor GC 失败,并抛出 OutOfMemoryError: GC overhead limit exceeded 或直接 Java heap space。
- 常见诱因:Survivor 过小(
-XX:SurvivorRatio=16导致 S 区仅占年轻代 1/18)、对象生命周期分布陡峭(大量对象活过 2–3 次 GC) - 调大 Survivor 不一定缓解问题——反而延长复制耗时、增加停顿,关键是匹配应用的对象年龄分布曲线
- 大对象(>
-XX:PretenureSizeThreshold)压根不进 Survivor,直接去老年代,绕过整个年龄逻辑
真正影响晋升的从来不是“年龄阈值”本身
很多人盯着 -XX:MaxTenuringThreshold 调来调去,却没意识到:JVM 晋升决策是空间压力、对象大小、年龄分布三者实时博弈的结果。年龄只是默认兜底策略之一,常被动态规则覆盖。
比如你设了 -XX:MaxTenuringThreshold=15,但应用里 80% 的对象活不过 3 次 GC,剩下 20% 在第 4 次 GC 时就因“相同年龄对象超 Survivor 半区”被集体推进老年代——这时阈值形同虚设。
观察真实晋升行为,得看 GC 日志里的 Desired survivor size、age 分布直方图,以及每次 Minor GC 后 tenured 列的增长量,而不是只信配置项。










