跳到主要内容
Cowers://
全部文章
Python

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% 情况 补漏循环引用