跳至正文
来两杯美式
返回

GC 怎么判断对象「该死了」:引用计数 vs 可达性分析

By 来两杯美式
发布于

这是「一文拿下 GC」系列第二篇。GC 的第一步不是回收,而是判定:哪些对象是「活」的、必须留下;哪些是「死」的、可以回收。主流语言(Java/C#/Go)都在用可达性分析,但引用计数也有自己的应用场景。

一、引用计数法

应用语言:Perl、Python、PHP 等。

1.1 核心思想

图中绿色的云(GC ROOTS)表示程序正在使用的对象。从技术上讲,这些可能是当前正在执行的方法中的局部变量,或者是静态变量一类。

引用计数法:GC Roots + 循环引用组(红色虚线圈)

1.2 优点

1.3 缺点

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 循环引用的解决方案

二、可达性分析算法

应用语言:Java、C#、Go 等主流语言。

2.1 核心思想

通过一系列被称为 GC Roots 的对象作为起点,从这些节点向下搜索,搜索走过的路径成为引用链(Reference Chain):

可达性分析:蓝色为可达对象、灰色为不可达、红色为循环引用(Tracing GC 可回收)

2.2 关键概念

重要细节判定为「可回收」真正被回收之间还有一段时间。Java 对象在被判定为不可达后,会经历两次标记过程才会真正被回收(finalize() 方法给了对象「自我救赎」的机会)。

2.3 能作为 GC Roots 的对象

三、两种算法的对比

维度引用计数法可达性分析
实现简单复杂(需要 STW)
判定效率高(O(1))较低(需要遍历对象图)
循环引用无法处理可处理
内存开销每个对象一个计数器只需维护 GC Roots
实时性引用失效立即回收GC 触发时统一回收
代表语言Python、PHP、PerlJava、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 GC
第 2 / 5 篇
查看系列全部文章
  1. 01.GC 调优入门:参数、工具与参考值
  2. 02.GC 怎么判断对象「该死了」:引用计数 vs 可达性分析
  3. 03.垃圾收集算法三板斧:标记-清除、复制、标记-整理
  4. 04.垃圾收集器大阅兵:Serial/ParNew/Parallel/CMS/G1
  5. 05.堆内存区域全景:Eden/Survivor/Old + PermGen/Metaspace 演进

上一篇
垃圾收集算法三板斧:标记-清除、复制、标记-整理
下一篇
GC 调优入门:参数、工具与参考值