- 引用计数(Reference Counting)
- 标记-清除(Mark-Sweep)
- 复制算法(Scavenge / Copying Collection)
一、引用计数(Reference Counting)
1)算法要解决的核心问题
GC 首先要回答:哪些对象是垃圾?
引用计数用最直观的定义来判断:
垃圾 = 没有任何引用指向它的对象
2)基本机制
给每个对象维护一个计数器 ref_cnt:
- 新增一个引用 →
ref_cnt++ - 断开一个引用 →
ref_cnt-- ref_cnt == 0→ 该对象可立即回收
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 → 无法回收
引用计数的核心失败点:它只看“引用次数”,不看“从根是否可达”。
二、Mark-Sweep(标记-清除)
1)算法要解决的核心问题
引用计数不可靠(循环引用),于是 Mark-Sweep 改用更稳的标准:
垃圾 = 从根集(Root Set)不可达的对象
也就是说:
不是问“有没有引用”,而是问“能不能从根走到”。
2)算法流程(两阶段)
(1)Mark:标记存活对象
- 从 Root Set 出发遍历对象图(DFS/BFS 都行)
- 能访问到的对象都标记为 “live”
(2)Sweep:清除未标记对象
- 扫描堆/对象列表
- 未标记的对象直接释放
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)算法流程(复制为核心)
- 从 Root Set 出发遍历
- 遇到存活对象 → 复制到 To Space
- 在 To Space 中按复制顺序排列(天然连续,无碎片)
- 复制完成后:From Space 整块直接清空
- 交换 From / To 的角色
4)优点
- 无碎片(To Space 连续)
- 回收成本主要与“存活对象数量”相关(活的少则很快)
- 分配可以很快(bump pointer)
5)缺点
- 空间换时间:需要预留一块 To Space(额外空间开销)
- 若存活率高,复制成本会变得很大(搬运太多)
四、三种算法“复盘对照表”(脱离分代语境)
| 算法 | 垃圾判定标准 | 是否移动对象 | 是否需要全堆扫描 | 是否能处理循环引用 | 主要代价 |
|---|---|---|---|---|---|
| 引用计数 | 引用次数为 0 | 否 | 否(但有递归回收) | ❌ 否 | 每次赋值维护计数;循环引用 |
| Mark-Sweep | 根不可达 | 否 | 是(Mark + Sweep) | ✅ 是 | 碎片;扫描成本 |
| Scavenge | 根不可达 | ✅ 是(复制) | 不扫“整个空间”,主要追踪可达对象 | ✅ 是 | 额外空间;存活率高时复制贵 |
五、一句话把三者的“思想差异”钉住
- 引用计数:盯着“引用数量”,想做到“对象没人要就立刻死”
- Mark-Sweep:盯着“根可达性”,用“图遍历”定义生死
- Scavenge:同样用“根可达性”,但策略是“只搬活的,剩下整块丢掉”