【从源码角度深度理解 CPython 的垃圾回收机制】:第2课循环引用:标记清除-分代回收
二. 循环引用的终结者:标记-清除(Mark-Sweep)
通过引用计数,我们已经处理了一部分引用个数为0的数据,但是还有一部分循环引用的对象没有被清理。这个如何处理呢?
为了解决循环引用问题,Python 引入了标记-清除(Mark-Sweep)算法,作为引用计数的补充。
2.1 标记-清除的基本原理
标记-清除算法分为两个阶段:
- 标记(Mark):从“根对象”(如全局变量、栈上的局部变量)出发,遍历所有可达对象,并给它们打上“存活”标记。
- 清除(Sweep):扫描所有被 GC 跟踪的对象,回收那些未被标记的对象(即不可达对象)。
2.2 什么是“根对象”?
“根对象”是程序可以直接访问的起点,包括:
- 全局变量
- 当前函数的局部变量
- 调用栈中的参数
- 内置模块、类、函数等
GC 从这些“根”出发,像洪水一样“淹没”所有能到达的对象。
2.3 GC 跟踪的对象
并非所有对象都会被 GC 扫描。只有那些可能参与循环引用的容器类对象才会被 GC 跟踪,例如:
list
、dict
、set
- 自定义类的实例
tuple
(如果包含可变对象)
而 int
、str
、float
等原子类型不会被 GC 跟踪,因为它们无法形成循环引用。
你可以通过 gc.is_tracked(obj)
来检查一个对象是否被 GC 跟踪。
a = [1, 2, 3] # list → 被 GC 跟踪 ✅
b = {'x': a} # dict → 被 GC 跟踪 ✅
c = MyClass() # 自定义实例 → 被 GC 跟踪 ✅
d = 42 # int → 不被 GC 跟踪 ❌
e = "hello" # str → 通常不被跟踪(除非驻留)❌
f = (a, b) # tuple 包含可变对象 → 被跟踪 ✅
g = (1, 2, 3) # tuple 只含不可变 → 可能不被跟踪 ❌
举例
import gcprint(gc.is_tracked([1,2,3])) # True
print(gc.is_tracked(42)) # False
print(gc.is_tracked("hello")) # False (通常)
print(gc.is_tracked(( [1], ))) # True
2.4 细节追问
- 问题总结
问题 |