复制算法依赖双survivor区实现无碎片回收,eden:from:to=8:1:1兼顾吞吐与弹性;survivor利用率取决于对象存活率与年龄分布,需通过gc日志观察占用率、晋升年龄及minor gc指标动态调优。

复制算法在 Survivor 区发挥作用,核心不是堆更多内存进去,而是让每次 Minor GC 后的存活对象“走得更稳、留得更准、升得更少”。空间利用率高,不等于 Survivor 区越大越好;回收效果好,也不单看 GC 次数减少——关键在对象生命周期与区域容量之间的动态匹配。
两个 Survivor 区是复制算法落地的前提
单个 Survivor 区无法支持复制算法连续运行:一次 GC 后,若所有存活对象都挤进同一个区,下一轮就得一边清理一边写入,只能退化成标记-清除,必然产生碎片。而双区(From/To)交替使用,保证任一时刻总有一个 Survivor 是空的,复制过程天然紧凑、无碎片。Eden:From:To = 8:1:1 的默认比例,正是兼顾分配吞吐(大 Eden)与复制弹性(双小 Survivor)的结果。
空间利用率取决于对象存活率与年龄分布
真正影响 Survivor 区“有没有被用满”的,不是总大小,而是每轮 GC 后有多少对象能活下来、活几轮。如果多数对象在 age=1 就晋升,说明 Survivor 容量远超实际需要;如果大量对象卡在 age=2–3 且频繁触发“年龄阈值提前晋升”(日志中出现 age=2 threshold=2),则表明当前 Survivor 有效承载力不足——不是空间小,而是“高龄残留”占位太多,新存活对象没地方放。
- 观察 GC 日志里每轮 Survivor 使用量(如 PSYoungGen: 8192K->1024K(9216K)),关注“->”后数值是否持续逼近上限
- 检查晋升对象的年龄分布:-XX:+PrintGCDetails 中的 age 行会列出各年龄对象大小,若 age=1 占比过高,需排查短命对象被意外“带活”
- 避免在方法内创建大临时对象(如 new byte[1MB]),这类对象常绕过 Survivor 直接进入老年代,反而挤压 Survivor 配置空间
回收效果由动态策略而非静态参数决定
JVM 不死守 MaxTenuringThreshold。当某次 GC 后,某个年龄的所有对象总和超过 Survivor 空间一半,JVM 会把 ≥ 该年龄的对象全部晋升——这是空间换时间的主动妥协。它防止因几个“钉子户”对象长期滞留,导致整个 Survivor 区失效。这个机制默认启用(-XX:+UseAdaptiveSizePolicy 开启时),无需手动打开,但必须通过日志确认是否生效。
- 若发现 age=1 就批量晋升,优先调低 -XX:MaxTenuringThreshold(比如设为 1),而不是盲目调大 Survivor
- 开启自适应策略后,JVM 会根据历史 GC 数据自动收缩 Eden、扩大 Survivor——前提是 GC 日志数据足够稳定,至少经历 5–10 次 Minor GC
- 慎用固定 -XX:SurvivorRatio:压测中对象存活率稳定再调;日常环境建议依赖自适应,让 JVM 自己学
监控比配置更重要
Survivor 区不是调参目标,而是观测窗口。它的使用模式直接反映应用对象生命周期是否健康。重点关注三项指标:
- Survivor 空间占用率波动幅度:长期高于 80% 或频繁触顶,说明对象存活异常或 Survivor 实际容量不足
- 晋升到老年代的对象大小与年龄:大量 age
- Minor GC 频率与 STW 时间变化:调大 Survivor 后 GC 更少但单次更长?说明 Eden 被压缩过度,需回调











