标记整理
标记整理分为两个步骤
1.标记阶段:与标记-清除算法完全一致。遍历所有可达对象(从 GC Roots 开始),标记它们为“存活”。
2.整理阶段:不再是简单清除垃圾对象,而是将所有存活的对象向内存空间的一端(通常是起始地址或结束地址)移动,紧凑排列。移动完后,边界之外的内存空间全部视为空闲空间,可以一次性分配。

算法步骤详解
- 暂停应用程序线程:
- 同样需要 “Stop-The-World” 停顿,以确保对象引用关系在标记和移动过程中保持稳定。标记-整理算法的 STW 时间通常比标记-清除更长,因为它包含了对象的移动开销。
- 标记阶段****:
- 起点:从GC Root开始。
- 遍历:采用深度优先搜索或广度优先搜索遍历对象图。
- 标记:将所有从GC Roots可达的对象标记为“存活”。结果与标记清除算法相同。
- 整理阶段****:
- 计算新地址:
- 遍历堆内存(通常是线性扫描)。
- 计算每个存活对象在整理后应该被移动到的新地址。新地址通常是连续的,从堆空间的某一端(如低地址端)开始排列。
- 一种常见的策略是:维护一个“指针”(
compaction pointer),初始指向目标区域(如堆起始地址)。每遇到一个存活对象,就计算其大小,将该对象的新地址设置为当前指针位置,然后将指针向后移动该对象大小的距离。
- 更新引用:
- 由于对象被移动了位置,所有指向这些移动对象的引用(包括GC Root中的引用和存活对象内部字段的引用)都需要被更新为对象的新地址。
- 再次遍历对象图(从GC Roots开始),访问所有存活对象及器引用,将指向被移动对象的引用值修改为计算好的新地址。
- 关键点:更新引用必须在对象实际移动之前完成,或者在移动过程中使用特殊的技巧(如 Brooks 指针)来保证引用的正确性。否则,移动对象后,旧的引用就会指向无效地址。
- 移动对象:
- 遍历堆内存
- 将每个存活的对象复制(移动)到之前计算好的新地址
- 移动完成后,所有存活对象都被紧密地排列在堆空间的一端(如低地址端)。
- 回收空间:
- 移动完成后,从最后一个存活对象之后的位置开始,直到堆空间的另一端(如高地址端),所有内存空间都是连续的空闲空间。
- 这个连续的大块空闲内存可以被一个简单的指针(如
bump pointer)管理,新对象的分配变得极其高效(只需移动指针并清零内存)。
优缺点
- 优点:
- 解决内存碎片: 这是最核心的优势!通过将存活对象紧凑排列,消除了标记-清除算法产生的内存碎片问题。分配新对象时,只需要在连续空闲空间的末尾进行指针碰撞(
bump-the-pointer),分配速度非常快且简单。 - 空间利用率高: 消除了碎片浪费,堆空间得到更有效的利用。特别是对于需要分配大对象的场景,成功率更高。
- 局部性原理提升: 紧凑排列的对象在内存中位置相邻,更有可能被加载到 CPU 缓存同一缓存行(Cache Line)中。这可以提升程序访问对象的效率(缓存命中率提高)。
- 缺点:
- STW 时间更长: 移动对象和更新引用的开销通常比简单的清除操作要大得多。这导致 Stop-The-World 停顿时间通常比标记-清除算法更长,尤其是在堆很大、存活对象很多的情况下。这是标记-整理算法最主要的缺点。
- 移动开销: 复制对象本身需要时间,尤其是对于大对象。
- 更新引用开销: 需要遍历整个对象图来更新所有指向被移动对象的引用,这也是一个耗时的操作。
- 实现更复杂: 需要精确地处理对象移动和引用更新,实现难度高于标记-清除算法。

浙公网安备 33010602011771号