Python 内存管理:引用计数、垃圾回收与弱引用
梳理 CPython 的引用计数、循环引用、分代垃圾回收与内存管理机制。
CPython 的内存管理 = 引用计数(主力)+ 分代 GC(补漏)
一、什么是 CPython
Python 是一门语言规范,可以有多种实现。CPython 是官方实现,用 C 语言写成,也是你 python xxx.py 时默认运行的那个。
你写的 .py
↓ 编译
字节码 .pyc
↓ 解释执行
CPython 虚拟机(C 语言)NOTE 类比 就像 Java 规范有 Oracle JDK / OpenJDK 等不同实现,Python 也有 PyPy、Jython、MicroPython 等。引用计数和 GC 是 CPython 的实现细节,不是 Python 语言规范的一部分。
二、引用计数(Reference Counting)
核心思路
每个对象内部有一个 ob_refcnt 字段,记录"有多少人指向我"。引用数降到 0 时,对象立刻被销毁、内存立刻释放。
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2(变量 a + getrefcount 的临时引用)
b = a # refcnt → 3
del b # refcnt → 2引用数 +1 / -1 的时机
| +1(引用增加) | -1(引用减少) |
|---|---|
赋值给变量 a = obj |
del a |
放入容器 list.append(obj) |
变量重新绑定 a = other |
| 作为函数参数传入 | 容器销毁或移除元素 |
| 作为返回值 | 函数结束,局部变量消失 |
优点
- 即时回收,不需要等待
- 确定性强(对象的析构时机可预测)
三、引用计数的盲区:循环引用
a = []
b = []
a.append(b) # a 引用 b
b.append(a) # b 引用 a
del a
del b
# 两者 refcnt 都是 1(互相引用),永远不会变 0 → 内存泄漏!两个对象互相"拉住"对方,从外部已经没有任何路径能访问到它们,却因为 refcnt ≠ 0 而无法被引用计数回收。
四、分代垃圾回收(Generational GC)
为什么分代?
大多数对象生命周期很短,少数对象长期存活。分代可以减少不必要的扫描。
Gen 0 → 新对象,扫描最频繁(阈值 700 个对象)
Gen 1 → 从 Gen 0 存活下来的
Gen 2 → 长期对象(全局变量、模块等),很少扫描GC 如何检测循环引用?
GC 不修改真实的 refcnt,而是用一个临时副本 gc_refs 来推导。
三步算法:
① 复制 refcnt → gc_refs
list_A.gc_refs = 1
list_B.gc_refs = 1② 遍历所有对象,对其内部引用的对象执行 gc_refs -= 1 (即:把"来自内部循环"的引用全部抵消掉)
遍历 list_A,它引用了 list_B → list_B.gc_refs -= 1 = 0
遍历 list_B,它引用了 list_A → list_A.gc_refs -= 1 = 0③ 检查 gc_refs
| 结果 | 含义 |
|---|---|
gc_refs > 0 |
有外部引用,对象还活着,不回收 |
gc_refs == 0 |
所有引用来自内部循环,是垃圾,回收 |
TIP 直觉理解 这个算法有点像拓扑排序消除入度:把内部的引用"对冲"掉,剩下的才是真正来自外部的引用。
有外部引用时 GC 不会误杀
a = []
b = []
a.append(b)
b.append(a)
# 不 del a,不 del b此时:
list_A.refcnt = 2 → gc_refs 初始为 2
list_B.refcnt = 2 → gc_refs 初始为 2
互相抵消后:
list_A.gc_refs = 1 → 有外部引用,存活!
list_B.gc_refs = 1 → 有外部引用,存活!GC 正确判断它们是活的,不回收。✅
五、综合案例分析
a = [1]
b = [2]
a.append(b) # list_B.refcnt = 2(变量 b + list_A 内部)
b.append(a) # list_A.refcnt = 2(变量 a + list_B 内部)
del b # list_B.refcnt = 2 - 1 = 1(只剩 list_A 内部引用)
print(a) # [1, [2, [...]]] ← Python 检测到循环,用 [...] 表示del b 之后的引用结构:
变量 a → list_A (refcnt=2)
↓
list_B (refcnt=1)
↓
list_A(循环)list_B 没有被释放,因为 list_A 内部还引用着它。
此时若 GC 运行(实际上不会,因为有外部引用存在):
gc_refs 抵消后:
list_A.gc_refs = 1 → 有外部变量 a,存活
list_B.gc_refs = 0 → 但被 list_A 可达,随 list_A 一起存活WARNING 关键点 gc_refs = 0 不等于立刻被回收。GC 会从所有 gc_refs > 0 的活对象出发做可达性标记,凡是能访问到的都不回收。list_B 虽然 gc_refs = 0,但从 list_A 可以访问到,所以被"救活"。
只有执行 del a 之后,list_A 和 list_B 才真正变成不可达的孤岛,GC 才会将它们一起回收。
六、实用 API
import gc
gc.collect() # 手动触发 GC(一般不需要)
gc.get_threshold() # 查看各代触发阈值,默认 (700, 10, 10)
gc.disable() # 关闭 GC(对性能极敏感的场景,如 Instagram)
gc.enable() # 重新开启 GC七、总结
普通对象 → 引用计数处理(即时、确定性强)
循环引用 → 引用计数失效,由分代 GC 补救(有延迟)| 引用计数 | 分代 GC | |
|---|---|---|
| 处理对象 | 所有对象 | 仅循环引用 |
| 回收时机 | 引用降到 0 立即回收 | 阈值触发或手动调用 |
| 速度 | 快 | 相对慢 |
| 覆盖范围 | 99% 情况 | 补漏循环引用 |