☰
Python 内存管理机制全面分析
2026/10/8 20:55:59 网站建设 项目流程

前言


关于 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 了"这个说法不成立。


常见坑点



  1. 以为循环引用靠引用计数就能清掉


❌


``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 兜底 ```



  1. 依赖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)``



  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() ```



  1. 用__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() ```



  1. __slots__类上直接做弱引用


❌


```python # 适用于 Python 3.8+ import weakref


class P: __slots__ = ("x",)


weakref.ref(P()) # TypeError ```


✅


``python # 适用于 Python 3.8+ class P: __slots__ = ("x", "__weakref__")``



  1. 关掉 GC 后测试用gc.collect()又开着自动回收


❌


```python # 适用于 Python 3.8+ import gc


gc.disable() gc.collect() # 手动能收,但自动回收仍关着,容易误判 ```


✅


```python # 适用于 Python 3.8+ import gc


print(gc.isenabled()) # 先确认自动回收的状态 ```



  1. 以为小整数一定共享


❌


``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()

GILCPython 实现细节,非语言规范3.13 起有实验性自由线程构建



把内存管理记成一句话即可:计数负责大部分,gc专治循环,弱引用用来断开不该持有的引用。所有具体数值(阈值、小整数缓存范围、永生对象的计数)都属于实现细节,写业务逻辑时不要依赖它们。





需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询