标记-清除与标记-整理的核心分歧在于空闲内存处理方式:前者保留碎片、不移动对象、无需更新引用;后者通过滑动迁移消除碎片,但必须更新所有引用、复制数据并承受硬件适配代价。

标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)虽同属追踪式垃圾回收算法,但二者在“如何处理存活对象后的空闲内存”这一关键环节上存在本质分歧——这不是渐进优化,而是设计哲学的分叉:一个选择“容忍碎片”,一个坚持“主动归并”。
核心分歧在于对“空闲空间”的处置逻辑
标记-清除仅做两件事:遍历对象图完成标记,再扫描堆内存释放未被标记的对象。它不移动任何存活对象,因此空闲内存以零散块(fragment)形式残留。这种策略实现简单、暂停时间短,但长期运行必然导致外部碎片化,影响大对象分配。
标记-整理则在标记之后增加第三阶段:计算每个存活对象的新地址,并统一向堆起始端滑动迁移。它用一次性的对象移动代价,换取连续可用空间。这直接消除了外部碎片,但带来三重开销:更新所有指向这些对象的引用、复制对象数据、重新计算布局。
引用更新是整理阶段不可绕过的硬性成本
标记-清除无需修改引用——对象位置不变,指针仍有效。而标记-整理必须确保迁移后程序逻辑不崩溃,这就要求:
- 在移动前,为每个存活对象记录其新地址(通常借助“转发指针”或额外位图)
- 遍历所有根集(栈、寄存器、全局变量)及对象内部字段,将旧地址替换为新地址
- 若采用“双遍历”策略(先计算偏移再批量复制),还需维护偏移映射表;若用“滑动压缩”,则需实时累加已移动字节数
这个过程无法被并发安全地省略或跳过,是逻辑上不可妥协的环节。
内存布局约束让整理算法更难适配现代硬件与运行时
标记-清除对内存页、缓存行、NUMA节点等无特殊要求;而整理操作天然倾向线性搬运,容易引发以下问题:
- 跨页移动对象可能触发多次缺页中断,尤其在大堆场景下显著拖慢暂停时间
- 频繁写入对象头(如设置转发指针)破坏CPU缓存局部性
- 在支持读屏障的增量回收器中,整理会干扰屏障逻辑——因为对象位置变更本身就是一个需要精确跟踪的“写操作”
因此,ZGC、Shenandoah 等新一代收集器宁可引入着色指针、加载屏障等复杂机制,也回避全堆整理。
不是“谁更好”,而是“谁更适合当前约束”
标记-清除适合堆较小、分配模式较均匀、或对停顿极度敏感的场景(如实时嵌入式系统);标记-整理则在长时间运行、需稳定分配大对象的服务端应用中仍有价值(如Serial Old、Parallel Old)。但二者之间没有平滑过渡路径——你不能给标记-清除“加个移动步骤”就变成整理算法,因为引用更新、对象头语义、并发控制等底层契约完全不同。
这种鸿沟不是工程细节的差异,而是内存管理模型层面的断裂:一个假设“地址恒定”,一个接受“地址可变”。











