Python内存管理:引用计数、垃圾回收机制、循环引用问题
聊到Python的内存管理,很多人第一反应就是「Python不是有自动垃圾回收吗?我不用管内存」。嗯,这话对了一半。自动回收确实省心,但你要是写过长时间运行的服务,或者处理过大量数据,就会明白——内存泄漏这玩意儿,Python里一样存在。
我个人习惯把Python的内存管理拆成三个层面来看:引用计数、垃圾回收、以及那个最让人头疼的循环引用。咱们一个一个说。
引用计数:最基础的内存管理手段
Python里每个对象都有一个引用计数。说白了,就是记录「有多少个变量在指着这个对象」。当引用计数降到0,内存就会被立即释放。
看个简单的例子:
import sys
a = []
print(sys.getrefcount(a)) # 输出 2(a 和 getrefcount 的临时引用)
b = a
print(sys.getrefcount(a)) # 输出 3
del b
print(sys.getrefcount(a)) # 输出 2
我在项目中遇到过一个问题:一个对象被多个地方引用,但某个引用忘记释放了,导致对象一直活着。这种问题用 gc.get_objects() 配合 sys.getrefcount 就能定位。
gc.get_referrers(obj) 可以查看谁在引用某个对象,排查泄漏时特别好用。
垃圾回收:引用计数的补充
引用计数有个硬伤——它搞不定循环引用。你想想看,两个对象互相指着对方,外部又没有变量引用它们,引用计数永远到不了0。这时候就需要垃圾回收器出马了。
Python的垃圾回收器主要做两件事:
- 检测循环引用:通过「标记-清除」算法,找出那些互相引用但外部不可达的对象
- 分代回收:把对象按存活时间分成三代,越老的对象越少被扫描
我建议你记住这个分代参数:
import gc
# 查看当前阈值
print(gc.get_threshold()) # 默认 (700, 10, 10)
# 手动触发垃圾回收
gc.collect()
第一代触发阈值是700,意思是新分配对象数减去回收对象数达到700时,就会触发一次第0代回收。第二代和第三代则是基于前一代的回收次数。
循环引用:最常见的泄漏元凶
循环引用长什么样?我直接给你看代码:
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def create_cycle():
a = Node("A")
b = Node("B")
a.children.append(b)
b.parent = a
# 函数结束,a和b互相引用,但外部没有引用它们
# 如果没有垃圾回收,这两个对象永远不会被释放
create_cycle()
# 此时 a 和 b 已经不可达,但引用计数不为0
我曾经在一个爬虫项目里遇到过类似问题。每个请求对象都持有response对象的引用,response又持有request的引用,结果内存蹭蹭往上涨。排查了整整一天,最后发现是回调函数里不小心形成了循环引用。
__del__ 方法,结果循环引用导致 __del__ 永远不会被调用。因为垃圾回收器无法确定循环引用中对象的销毁顺序。所以,尽量不要在 __del__ 里写重要逻辑。
如何检测和解决循环引用
检测循环引用,我一般用 gc 模块:
import gc
# 开启垃圾回收的调试
gc.set_debug(gc.DEBUG_SAVEALL)
# 创建循环引用
class A:
def __init__(self):
self.b = None
class B:
def __init__(self):
self.a = None
a = A()
b = B()
a.b = b
b.a = a
# 删除外部引用
del a
del b
# 手动触发回收
gc.collect()
# 查看被回收的对象
for obj in gc.garbage:
print(type(obj), repr(obj))
解决循环引用,常用的方法有:
- 使用弱引用:用
weakref模块,不增加引用计数 - 显式断开引用:在适当的时候手动设置
None - 使用上下文管理器:确保资源被正确释放
弱引用的用法:
import weakref
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
a = Node("A")
b = Node("B")
# 使用弱引用,不增加引用计数
b.parent = weakref.ref(a)
a.children.append(b)
# 现在即使删除 a,也不会因为循环引用导致内存泄漏
知识体系总览
下面这张图,是我梳理的Python内存管理核心逻辑。你看一眼就能明白整个体系是怎么串起来的:
说白了,Python的内存管理就是一套组合拳。引用计数负责快速释放,垃圾回收负责兜底,分代回收负责优化性能。你只要记住:大部分情况下你不需要操心,但一旦出现内存泄漏,先查循环引用。
gc.get_objects() 的监控,定期打印对象数量。一旦发现某个类的实例数异常增长,立刻就能定位问题。
好了,关于Python内存管理的三个核心概念,咱们就聊到这儿。记住一句话:引用计数是基础,垃圾回收是保障,循环引用是坑。下次你遇到Python进程内存只涨不降,先想想是不是哪个地方互相指着没放开。