这是「一文拿下 GC」系列第二篇。GC 的第一步不是回收,而是判定:哪些对象是「活」的、必须留下;哪些是「死」的、可以回收。主流语言(Java/C#/Go)都在用可达性分析,但引用计数也有自己的应用场景。
一、引用计数法
应用语言:Perl、Python、PHP 等。
1.1 核心思想
- 给对象添加一个引用计数器
- 每当有一个地方引用它,计数器加一
- 当引用失效时,计数器减一
- 任何时刻计数器为 0 的对象就是「不再被使用」,可以被回收
图中绿色的云(GC ROOTS)表示程序正在使用的对象。从技术上讲,这些可能是当前正在执行的方法中的局部变量,或者是静态变量一类。

1.2 优点
- 实现简单
- 判定效率高(加减操作 O(1))
1.3 缺点
- 无法解决循环引用(detached cycle)问题
- 存在内存浪费(每个对象都额外存一个计数器字段)
- 每次加减操作浪费系统性能
1.4 循环引用问题
# 经典循环引用
a = Node("A")
b = Node("B")
a.next = b # b 引用计数 +1
b.next = a # a 引用计数 +1
del a # a 引用计数 -1,但 b.next 还引用 a,所以 a 不为 0
del b # b 引用计数 -1,但 a.next 还引用 b,所以 b 不为 0
# 此时 a 和 b 都不再被外部使用,但引用计数都不为 0——永远不会被回收!
1.5 循环引用的解决方案
- 使用弱引用(Weak Reference)打破循环
- 使用另外的额外算法(如追踪式 GC)来排查循环引用
- Python 的 GC 模块实际上同时用了引用计数 + 标记-清除——为了解决循环引用
二、可达性分析算法
应用语言:Java、C#、Go 等主流语言。
2.1 核心思想
通过一系列被称为 GC Roots 的对象作为起点,从这些节点向下搜索,搜索走过的路径成为引用链(Reference Chain):
- 当一个对象对 GC Roots 没有任何引用链相连,说明从 GC Roots 到这个对象不可达
- 此对象就是不可用的,被判定为可回收对象

2.2 关键概念
- GC Roots:判定对象是否可达的起点
- 可达(reachable):从 GC Roots 出发,沿着引用链能找到
- 不可达(unreachable):从 GC Roots 出发,没有任何引用链可达
重要细节:判定为「可回收」和真正被回收之间还有一段时间。Java 对象在被判定为不可达后,会经历两次标记过程才会真正被回收(finalize() 方法给了对象「自我救赎」的机会)。
2.3 能作为 GC Roots 的对象
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(一般说的 Native 方法)引用的对象
三、两种算法的对比
| 维度 | 引用计数法 | 可达性分析 |
|---|---|---|
| 实现 | 简单 | 复杂(需要 STW) |
| 判定效率 | 高(O(1)) | 较低(需要遍历对象图) |
| 循环引用 | 无法处理 | 可处理 |
| 内存开销 | 每个对象一个计数器 | 只需维护 GC Roots |
| 实时性 | 引用失效立即回收 | GC 触发时统一回收 |
| 代表语言 | Python、PHP、Perl | Java、C#、Go |
四、为什么 Java 选可达性分析
Java 选可达性分析的核心原因是循环引用不可避免:
// Java 中同样会形成循环引用
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a; // 循环引用
a = null;
b = null;
// 此时 a 和 b 应该被回收——引用计数法做不到,可达性分析可以
如果用引用计数法,Java 的链表、树、图等数据结构用得又很多,循环引用是常态——整个 Java 生态会充斥着「内存泄漏」(其实是引用计数法泄漏,不是真的泄漏)。
小结
- 引用计数法实现简单但有致命循环引用问题,主流语言不用它做主要回收
- 可达性分析是 Java/C#/Go 等语言的事实标准,从 GC Roots 出发追踪可达性
- GC Roots 包括栈帧局部变量 + 类静态属性 + 常量 + JNI 引用
下一篇开始讲回收阶段——拿到「可回收对象名单」后,垃圾收集算法怎么把这些对象清掉。