前言
关于 Python 内存管理,流传最广的两句话是"Python 用垃圾回收自动管理内存"和"Python 已经没有 GIL 了"。第一句过于笼统,掩盖了真正的主角——引用计数;第二句是错的。
准确的说法是:CPython(也就是你平时用的那个 Python 解释器)以引用计数(reference counting)为主,分代垃圾回收器gc只负责回收容器对象之间的循环引用。而 GIL(全局解释器锁)是 CPython 的实现细节,至今仍是默认构建的一部分;3.13 起提供了实验性的自由线程(free-threading)构建,它不是默认,需要单独安装。
本文讲清引用计数、gc的分代策略、弱引用,以及哪些说法属于实现细节、不能依赖。
一、引用计数:主力机制
CPython 的每个对象都带一个计数器,记录有多少个引用指向它。引用加一、减一,减到零就立刻释放。因此绝大多数对象在失去最后一个引用的那一刻就被回收了,不需要等垃圾回收器。
# 适用于 Python 3.8+
import sys
class Demo:
def __del__(self):
print("Demo 被回收")
d = Demo()
print(sys.getrefcount(d)) # 通常为 2
del d # 立即打印:Demo 被回收关于sys.getrefcount()有两个必须知道的细节:
- 它会把返回值加一,因为对象被当作参数传入时多了一个临时引用。官方文档明确说明:返回的计数通常比你预期的高一,而且不要依赖这个具体数值,除了 0 或 1,它并不保证准确。
- 有些对象是永生对象(immortal object),引用计数是一个极大的常数,不代表真实引用数。Python 3.12 起引入这一优化。
| 动作 | 对引用计数的影响 |
|---|
赋值b = a | 对象计数 +1 |
传参f(a) | 调用期间 +1,返回后 -1 |
放进容器lst.append(a) | +1 |
del a | -1 |
| 离开作用域 | 局部引用 -1 |
重新赋值a = None | 原对象 -1 |
因为计数归零即释放,所以循环引用就是引用计数的死穴:两个对象互相引用时,各自的计数永远到不了零。
二、gc模块:回收循环引用
Python 的循环垃圾回收器(cyclic garbage collector)负责找出"只被彼此引用、与外界断联"的对象组,并把它们回收。它只跟踪容器对象,像int、str这类原子类型不参与循环检测——因为它们不可能持有对别的对象的引用。
# 适用于 Python 3.8+
import gc
print(gc.is_tracked(0)) # False(原子类型不跟踪)
print(gc.is_tracked("a")) # False
print(gc.is_tracked([])) # True(列表是容器)
print(gc.is_tracked({})) # False(空字典有优化,不被跟踪)
print(gc.is_tracked({"a": 1})) # True注意空字典那条:文档特别说明有些类型相关优化会抑制跟踪,所以判断"是否被跟踪"请以gc.is_tracked()的实际返回为准,别凭类型猜。
gc采用分代回收(generational collection):新对象进入第 0 代,活过一次回收就晋升到第 1 代,再活一次进入第 2 代。回收频率由阈值控制。
# 适用于 Python 3.8+
import gc
print(gc.get_threshold()) # 形如 (700, 10, 10),具体数值属实现细节
print(gc.get_count()) # 当前的分配/回收计数三元组,形如 (n0, n1, n2)
gc.collect() # 强制做一次全量回收,返回回收对象数
stats = gc.get_stats()
print(len(stats)) # 3,对应三个代gc.get_threshold()返回(threshold0, threshold1, threshold2)。当"分配数减回收数"超过threshold0时启动一次第 0 代回收;第 0 代被检查的次数超过threshold1次后会连带检查第 1 代。具体默认数值属于 CPython 实现,随时可能调整,请用gc.get_threshold()现场读取。
| 函数 | 作用 |
|---|
gc.collect(generation=2) | 触发回收,返回回收对象数与不可回收数之和 |
gc.get_threshold() | 读取当前三代阈值 |
gc.set_threshold(...) | 调整阈值,threshold0设为 0 即关闭回收 |
gc.get_count() | 读取当前三代计数 |
gc.get_stats() | 读取每代的回收统计 |
gc.get_objects() | 列出被跟踪的对象(调试用,代价高) |
gc.is_tracked(obj) | 对象是否被跟踪 |
gc.disable()/gc.enable() | 关闭 / 开启自动回收 |
gc.isenabled() | 自动回收是否开启 |
三、什么时候该动gc
官方文档的立场很明确:因为垃圾回收器是补充引用计数的,如果你的程序确定不产生循环引用,可以关掉它。但盲目关闭风险很大——循环引用一旦产生就会持续泄漏。
合理的用法是:
- 调试内存泄漏时用
gc.set_debug(gc.DEBUG_LEAK),它包含gc.DEBUG_SAVEALL,会把被回收的对象留在gc.garbage里供检查; - 批量创建大量临时对象前,可以在关键时刻手动
gc.collect()一次; - 关闭自动回收(
gc.disable())只适合"完全无循环、且对延迟敏感"的短流程,用完务必gc.enable()。
# 适用于 Python 3.8+
import gc
was_enabled = gc.isenabled()
gc.disable()
try:
# 一段确定没有循环引用的高性能代码
pass
finally:
if was_enabled:
gc.enable()四、弱引用:不阻止回收的引用
weakref模块提供弱引用(weak reference):持有弱引用不会增加对象的引用计数,因此不会阻止对象被回收。当对象被回收后,弱引用会变成"死引用",调用它返回None。
# 适用于 Python 3.8+
import weakref
class Node:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"Node({self.name!r})"
n = Node("A")
r = weakref.ref(n)
print(r()) # Node('A')
del n
print(r()) # None,对象已回收弱引用在缓存、观察者列表、父子互相引用的场景里非常有用。常用的还有weakref.WeakValueDictionary、weakref.WeakKeyDictionary、weakref.WeakSet和weakref.finalize。
注意:用__slots__的类默认不支持弱引用,除非把"__weakref__"也列进__slots__。
五、关于 GIL 与内存
GIL 是 CPython 的实现细节,它让同一进程内同一时刻只有一个线程执行 Python 字节码,也因此简化了对引用计数等内部状态的保护。它带来两个后果:
- 多线程对CPU 密集任务没有加速效果;
- 多线程对I/O 密集任务有效,因为等待 I/O 时会释放 GIL。
CPU 密集任务应改用multiprocessing或原生扩展。CPython 3.13 起提供了实验性的自由线程构建(PEP 703),它需要单独构建或安装,不是默认构建,其他实现(例如 Jython、IronPython)本来就没有 GIL。所以"Python 已经没有 GIL 了"这个说法不成立。
常见坑点
- 以为循环引用靠引用计数就能清掉
❌
``python # 适用于 Python 3.8+ a = {} b = {} a["b"] = b b["a"] = a del a, b # 以为此刻就释放了``
✅
```python # 适用于 Python 3.8+ import gc
a = {} b = {} a["b"] = b b["a"] = a del a, b gc.collect() # 循环引用要靠 gc 兜底 ```
- 依赖
getrefcount()的具体数字
❌
```python # 适用于 Python 3.8+ import sys
x = [] if sys.getrefcount(x) == 1: print("没有别的引用") # 判断依据不成立 ```
✅
``python # 适用于 Python 3.8+ # 只把 0/1 之外的值当参考,别写进逻辑 print(sys.getrefcount(x) >= 1)``
- 为省内存随手
gc.disable()却忘了恢复
❌
```python # 适用于 Python 3.8+ import gc
gc.disable() # 后面再没有 enable(),循环引用持续泄漏 ```
✅
```python # 适用于 Python 3.8+ import gc
gc.disable() try: pass finally: gc.enable() ```
- 用
__del__当作可靠的清理钩子
❌
``python # 适用于 Python 3.8+ class F: def __del__(self): self.close() # 不保证及时、不保证被调用``
✅
```python # 适用于 Python 3.8+ class F: def close(self): ...
def __enter__(self): return self
def __exit__(self, *exc): self.close() ```
__slots__类上直接做弱引用
❌
```python # 适用于 Python 3.8+ import weakref
class P: __slots__ = ("x",)
weakref.ref(P()) # TypeError ```
✅
``python # 适用于 Python 3.8+ class P: __slots__ = ("x", "__weakref__")``
- 关掉 GC 后测试用
gc.collect()又开着自动回收
❌
```python # 适用于 Python 3.8+ import gc
gc.disable() gc.collect() # 手动能收,但自动回收仍关着,容易误判 ```
✅
```python # 适用于 Python 3.8+ import gc
print(gc.isenabled()) # 先确认自动回收的状态 ```
- 以为小整数一定共享
❌
``python # 适用于 Python 3.8+ a = 1000 b = 1000 print(a is b) # 不保证``
✅
``python # 适用于 Python 3.8+ print(a == b) # 比较值用 ==;小整数缓存是实现细节``
总结
| 机制 | 职责 | 关键接口 |
|---|
| 引用计数 | 主力,计数归零立即回收 | sys.getrefcount() |
| 循环 GC | 兜底,回收容器间的循环引用 | gc.collect() |
| 分代策略 | 0/1/2 三代,按阈值触发 | gc.get_threshold() |
| 弱引用 | 不增加计数,不阻止回收 | weakref.ref() |
| GIL | CPython 实现细节,非语言规范 | 3.13 起有实验性自由线程构建 |
把内存管理记成一句话即可:计数负责大部分,gc专治循环,弱引用用来断开不该持有的引用。所有具体数值(阈值、小整数缓存范围、永生对象的计数)都属于实现细节,写业务逻辑时不要依赖它们。