垃圾回收三种经典算法复盘:引用计数、Mark-Sweep、Scavenge

34 阅读4分钟
  • 引用计数(Reference Counting)
  • 标记-清除(Mark-Sweep)
  • 复制算法(Scavenge / Copying Collection)

一、引用计数(Reference Counting)

1)算法要解决的核心问题

GC 首先要回答:哪些对象是垃圾?
引用计数用最直观的定义来判断:

垃圾 = 没有任何引用指向它的对象

2)基本机制

给每个对象维护一个计数器 ref_cnt

  • 新增一个引用 → ref_cnt++
  • 断开一个引用 → ref_cnt--
  • ref_cnt == 0 → 该对象可立即回收

image.png

3)Write Barrier(写屏障)的角色

引用关系的变化通常发生在“写对象字段 / 指针赋值”的瞬间。
因此虚拟机会在这类写操作旁边插入逻辑(伪代码):

void do_store(obj_field, new_value) {
    inc_ref(new_value);     // 新对象 +1
    dec_ref(old_value);     // 旧对象 -1(可能触发递归回收)
    obj_field = new_value;
}

关键注意点:先加后减
否则当 old_value == new_value 时,可能先减到 0 被回收,导致灾难。

4)优点

  • 及时回收:引用数归零立刻回收
  • 通常不需要专门 STW 的“全堆扫描” :回收工作被分散到赋值路径中(但多线程下仍需同步)

5)缺点(两条必须记牢)

  • 赋值成本高:每次引用变更都要维护计数,且可能递归回收
  • 循环引用致命:A↔B 互相引用但外部不可达,ref_cnt 都不为 0 → 无法回收

image.png

引用计数的核心失败点:它只看“引用次数”,不看“从根是否可达”。


二、Mark-Sweep(标记-清除)

1)算法要解决的核心问题

引用计数不可靠(循环引用),于是 Mark-Sweep 改用更稳的标准:

垃圾 = 从根集(Root Set)不可达的对象

也就是说:
不是问“有没有引用”,而是问“能不能从根走到”。

2)算法流程(两阶段)

(1)Mark:标记存活对象

  • 从 Root Set 出发遍历对象图(DFS/BFS 都行)
  • 能访问到的对象都标记为 “live”

(2)Sweep:清除未标记对象

  • 扫描堆/对象列表
  • 未标记的对象直接释放

image.png

3)Root Set 是什么

根集就是 GC 的起点,常见包括:

  • 栈上的局部变量引用(线程栈)
  • 寄存器中的引用
  • 全局/静态变量
  • 运行时持有的根对象(不同语言/VM略有差异)

4)优点

  • 能处理循环引用(只要从根不可达,就能回收)
  • 不要求对象移动(实现相对直接)

5)主要问题:内存碎片

Sweep 清掉垃圾后,空洞散落在各处 → 碎片化
碎片会导致:

  • 大对象分配困难
  • 分配/管理成本上升

所以才会出现 Mark-Compact(标记-整理):在 Mark 后把活对象“挤到一起”,消除碎片。


三、Scavenge(复制算法 / Copying Collection)

1)算法要解决的核心问题

Mark-Sweep 的问题之一是碎片,另一个是“清理过程需要扫大量空间”。
复制算法走了另一条路:

我不在原地清垃圾,我只把活的搬走。

2)基本结构:From / To 两块空间

把管理区域分成两半:

  • From Space:当前分配与存活对象所在地
  • To Space:空白区域,用于复制

3)算法流程(复制为核心)

  1. 从 Root Set 出发遍历
  2. 遇到存活对象 → 复制到 To Space
  3. 在 To Space 中按复制顺序排列(天然连续,无碎片
  4. 复制完成后:From Space 整块直接清空
  5. 交换 From / To 的角色

image.png

4)优点

  • 无碎片(To Space 连续)
  • 回收成本主要与“存活对象数量”相关(活的少则很快)
  • 分配可以很快(bump pointer)

5)缺点

  • 空间换时间:需要预留一块 To Space(额外空间开销)
  • 若存活率高,复制成本会变得很大(搬运太多)

四、三种算法“复盘对照表”(脱离分代语境)

算法垃圾判定标准是否移动对象是否需要全堆扫描是否能处理循环引用主要代价
引用计数引用次数为 0否(但有递归回收)❌ 否每次赋值维护计数;循环引用
Mark-Sweep根不可达是(Mark + Sweep)✅ 是碎片;扫描成本
Scavenge根不可达✅ 是(复制)不扫“整个空间”,主要追踪可达对象✅ 是额外空间;存活率高时复制贵

五、一句话把三者的“思想差异”钉住

  • 引用计数:盯着“引用数量”,想做到“对象没人要就立刻死”
  • Mark-Sweep:盯着“根可达性”,用“图遍历”定义生死
  • Scavenge:同样用“根可达性”,但策略是“只搬活的,剩下整块丢掉”