标记-复制仅用于新生代,依赖Eden与两块Survivor空间交替使用;标记-整理专用于老年代,通过紧凑排列避免碎片;二者不可互换,由分代与收集器绑定。

标记-复制算法只在新生代用,且必须有备用空间
标记-复制(Copying)不是全堆通用的策略,它依赖“一块正在用、一块空着”的前提。HotSpot里新生代的 Eden + 两块 Survivor(S0/S1)就是典型实现:每次 GC 时,把 Eden 和当前使用的 Survivor 中存活的对象,一次性复制到另一块 Survivor;然后清空源区域。这个过程不涉及指针更新以外的移动开销,也没有碎片问题。
但代价很实在:
-
Survivor区总容量只有新生代的 20%(默认 8:1:1),一旦存活对象超过这个阈值,就会触发Handle Promotion—— 直接晋升到老年代,而不是继续复制 - 如果对象平均存活率高(比如缓存类长生命周期对象意外进入新生代),复制成本飙升,GC 时间变长,甚至可能退化为 Full GC
- 不能用于老年代:老年代对象多、存活率高,复制全部存活对象开销不可接受
标记-整理专为老年代设计,移动对象但不换区域
标记-整理(Mark-Compact)和标记-复制一样分两阶段:先从 GC Roots 标记所有可达对象,但清除阶段不是“清空整块”,而是把所有存活对象往内存起始端紧凑排列,再调整它们的引用指针,最后清理边界外的连续空白区域。
这带来几个关键差异:
- 不需要预留一块同等大小的空闲内存 ——
Old Gen就是它自己的“备用空间” - 避免了内存碎片,让大对象分配更稳定;但移动对象本身有 CPU 开销,尤其是对象多、引用关系深时
- 所有主流老年代收集器(
Serial Old、Parallel Old、Shenandoah的初始标记后阶段)都含整理逻辑,只是实现方式不同(如 Shenandoah 用读屏障+并发整理)
别混淆“标记-清除”和“标记-整理”的触发条件
很多人看到 ConcurrentMarkSweep(CMS)已废弃,就以为“标记-清除”也淘汰了 —— 其实不是。CMS 是以牺牲整理为代价换取低延迟,它的“清除”阶段不做移动,所以必然产生碎片;而 G1 和 ZGC 虽然也用标记阶段,但后续是分区回收或着色指针+并发转移,不属于传统标记-清除或标记-整理。
真正还在用纯标记-清除的场景极少,常见于:
- 某些嵌入式 JVM 或早期 JDK 版本的老年代回收
-
Serial收集器搭配-XX:+UseSerialGC时,老年代默认用Serial Old(即标记-整理),但若显式加-XX:+UseMarkSweepGC才强制走标记-清除(JDK 9+ 已移除该参数) - 手动触发
jmap -histo或jstat观察到OC(Old Capacity)持续增长但OU(Old Used)没明显下降,可能是碎片堆积导致无法分配,背后就是未整理的清除行为
选哪个不是由你决定,而是由分代和收集器绑定
你写 -XX:+UseG1GC,JVM 就不会用标记-复制处理老年代,也不会用标记-整理处理新生代 —— G1 新生代用的是“分区复制”,老年代用的是“增量混合回收+局部整理”。同理,ZGC 完全跳出了这三类经典算法框架。
真正需要你干预的点其实很窄:
- 调优
SurvivorRatio影响复制能否成功,比如设成-XX:SurvivorRatio=4让两个 Survivor 占新生代 40%,降低晋升压力 - 观察
GC logs中是否频繁出现promotion failed或concurrent mode failure,前者指向复制失败,后者往往意味着老年代碎片+整理不及时 - 升级到 JDK 17+ 后,默认
UseZGC或UseShenandoahGC时,传统标记-复制/整理已不直接可见,但理解它们仍是读懂日志和堆转储的基础
算法本身没有高下,只有适用边界。错把标记-整理当成“更高级的复制”,或者指望在老年代强行启用复制,都是踩坑起点。











