标记-整理算法通过“移动存活对象至一端并同步更新所有指向它们的指针”消除外部碎片;分三轮扫描:标记与预计算地址、更新引用、移动对象,全程需stw以保证安全性。

标记-整理算法解决内存碎片的核心动作,就是“移动存活对象 + 同步修正所有指向它们的指针”。这不是简单的复制粘贴,而是一次有组织、分步骤、强依赖顺序的内存重排。
对象移动不是随机搬,而是向一端紧凑滑动
整理阶段不会把对象打乱重排,而是按某种确定顺序(常见是滑动顺序或线性顺序),将所有被标记为存活的对象,从堆起始地址开始依次紧挨着摆放。比如原来内存布局是:
[A][空][B][C][空][空][D](其中 A、B、C、D 存活,空格代表已死亡对象)
整理后变成:
[A][B][C][D][空][空][空]
所有存活对象被“挤”到左侧,右侧腾出一大块连续空闲空间。这个过程叫“压缩”(compaction),它直接消除了外部碎片——后续分配大对象时,只要空闲区够大,就能用指针碰撞方式快速分配,无需遍历空闲链表找合适小块。
指针更新必须全覆盖,漏一个就会导致悬垂引用
对象位置变了,但程序里还存着旧地址。如果不清除这些“过期指针”,下次访问就会读写错误内存。因此,整理阶段必须扫描并更新三类引用:
- 栈帧中的局部变量:当前线程栈里可能正拿着某个对象的引用
- 静态变量和常量池引用:类级别的长期持有者
- 其他存活对象内部的字段:比如 List 对象里存着 Node 的引用,Node 又引用下一个 Node
更新不是靠猜,而是靠“新地址映射表”:在计算阶段就为每个存活对象算好它将要搬去的新地址(如 A→0x1000,B→0x1018),之后遍历所有引用源,把原值替换成对应的新地址。这一步不可省略,也无法延迟——必须在对象移动完成前或同时完成,否则移动后旧地址内容已覆盖,无法反查。
移动与更新的执行顺序决定安全性和效率
典型实现会分三轮扫描堆:
- 第一轮:仅标记,并统计每个存活对象大小,顺带计算其目标起始地址(例如从 heap_start 开始累加)
- 第二轮:遍历所有引用位置(栈、静态区、对象字段),将每个引用的目标地址更新为预计算的新地址
- 第三轮:真正移动对象数据,并清除标记位
这种分离设计避免了边移动边更新带来的竞态风险。也正因如此,整个过程需 Stop-The-World:用户线程必须暂停,否则正在运行的代码可能读到半更新状态的引用,或在对象迁移中途访问旧地址。










