Python内存管理:引用计数机制、垃圾回收(GC)、循环引用问题
聊到Python的内存管理,很多人第一反应就是「Python有垃圾回收,不用管内存」。这话对了一半。我见过不少线上服务,就是因为对内存管理一知半解,结果内存涨上去就下不来,最后OOM被kill掉。
说白了,Python的内存管理就三板斧:引用计数、标记-清除、分代回收。咱们一个一个拆开看。
引用计数——最基础的记账本
每个Python对象内部都维护着一个整数,叫ob_refcnt。你每多一个变量指向它,这个数就+1;少一个引用,就-1。当计数归零,内存立即释放。
import sys
a = []
print(sys.getrefcount(a)) # 2,getrefcount自己也会加一次引用
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
这个机制的好处是实时、简单。坏处呢?循环引用——两个对象互相指着,外部没人用了,但引用计数永远不为0。
核心要点:引用计数是Python内存管理的基石,但它搞不定循环引用。这就是GC存在的意义。
循环引用——那个经典的坑
我在项目中遇到过最典型的场景:一个缓存系统,用字典存对象,对象里又持有字典的引用。跑着跑着,内存就炸了。
class Node:
def __init__(self):
self.parent = None
self.children = []
def create_cycle():
a = Node()
b = Node()
a.children.append(b)
b.parent = a # 循环引用形成
return a, b
# 函数返回后,a和b的外部引用没了
# 但a和b互相指着,引用计数都是1
# 内存泄漏了
你想想看,这种代码在业务里太常见了。树结构、图结构、双向链表、回调闭包……稍不注意就埋了雷。
避坑指南:我曾经排查过一个内存泄漏问题,查了两天才发现是ORM模型对象之间互相引用了。后来加了个weakref就解决了。记住:能用弱引用的地方,别用强引用。
垃圾回收——Python的清洁工
Python的GC主要负责处理引用计数搞不定的循环引用。它用的是标记-清除算法,配合分代回收来提升效率。
标记-清除(Mark-Sweep)
流程其实不复杂:
- 从根对象(全局变量、调用栈等)出发,遍历所有可达对象,打上标记
- 遍历堆内存,把没标记的对象——也就是不可达的——清掉
嗯,这里要注意:标记-清除会暂停程序执行。虽然Python做了优化,但如果你的对象数量巨大,这个停顿还是能感觉到的。
分代回收(Generational GC)
Python把对象分成三代:
| 代 | 特点 | 回收频率 |
|---|---|---|
| 第0代 | 新创建的对象 | 最频繁 |
| 第1代 | 熬过一次GC的对象 | 中等 |
| 第2代 | 熬过多次GC的对象 | 最低 |
这个设计的直觉是:大部分对象活不长。你想想看,函数里的临时变量,用完就扔。真正能活到第2代的对象,大概率会一直活着。所以没必要频繁扫描它们。
import gc
# 查看当前各代的对象数量
print(gc.get_count()) # (第0代数量, 第1代数量, 第2代数量)
# 手动触发一次GC
gc.collect()
# 调整阈值:第0代对象超过700时触发GC
gc.set_threshold(700, 10, 10)
个人习惯:我一般不会手动调GC参数,除非真的遇到性能瓶颈。但我会在关键业务代码里显式调用gc.collect()——比如处理完一批大对象后,主动清理一下,心里踏实。
GC调优——什么时候该出手
说实话,大部分Python应用不需要调GC。但如果你遇到以下情况,就得动手了:
- 内存持续增长,但业务逻辑没有明显泄漏
- GC停顿导致接口响应时间抖动
- 大量短生命周期对象频繁创建销毁
我建议的排查步骤:
- 先开
gc.set_debug(gc.DEBUG_LEAK),看看哪些对象没法回收 - 用
gc.get_objects()按类型统计,找到异常聚集的类 - 检查是不是有循环引用,能用
weakref就换 - 实在不行,调大GC阈值,减少回收频率
import gc
import collections
# 统计每种类型的对象数量
obj_counts = collections.Counter(type(o).__name__ for o in gc.get_objects())
for name, count in obj_counts.most_common(20):
print(f"{name}: {count}")
注意:别把GC完全关掉。我见过有人为了性能调优,直接gc.disable(),结果内存泄漏到系统崩溃。GC是安全网,不是性能瓶颈。
一张图看懂Python内存管理
下面这张图把引用计数、GC、循环引用的关系串起来了:
实战建议
最后,给几个我踩坑换来的建议:
- 能用
__slots__就用:减少每个对象的__dict__开销,省内存也省GC压力 - 大对象用完手动置None:帮助引用计数尽快归零
- 监控
gc.get_count():如果第0代对象数量异常高,说明有大量临时对象在折腾 - 别迷信
gc.disable():除非你100%确定没有循环引用,否则别关
我个人习惯在性能测试环境里,先跑一遍gc.set_debug(gc.DEBUG_STATS),看看GC触发的频率和耗时。数据说话,比瞎猜靠谱得多。