一、引言
python 作为一门高级语言,其自动内存管理机制让开发者从繁琐的 malloc/free 中解放出来。然而,“自动”并不意味着“无脑”——理解 python 的内存管理原理,不仅能写出更高效的代码,还能在遇到内存泄漏时快速定位问题。本文将深入剖析 python 的引用计数、循环引用、分代垃圾回收,并手把手教你使用 gc 模块进行排查。
二、引用计数:python 内存管理的基石
2.1 什么是引用计数?
每个 python 对象内部都有一个 ob_refcnt 字段,记录当前有多少个引用指向它。当引用计数降为 0 时,对象会被立即销毁,内存被回收。
import sys a = [] # 列表对象引用计数 = 1 b = a # 引用计数 = 2 print(sys.getrefcount(a)) # 输出 3(getrefcount 自身会临时增加一次引用) del b # 引用计数降为 1
2.2 引用计数的优缺点
| 优点 | 缺点 |
|---|---|
| 实时性高,对象不再被引用时立即回收 | 无法处理循环引用 |
| 实现简单,无需全局扫描 | 每次赋值、删除都会修改计数,有性能开销 |
| 确定性销毁,适合资源管理(如文件句柄) | 多线程下需要原子操作,增加锁竞争 |
2.3del删除的是引用,不是对象
del x 只是删除变量名 x 与对象的绑定关系,使对象的引用计数减 1。只有当引用计数归零时,对象才会被回收。
class test:
def __del__(self):
print("对象被销毁")
t = test()
t2 = t
del t # 只删除一个引用,不会触发 __del__
print("删除 t 后")
del t2 # 引用计数归零,触发 __del__注意:__del__ 方法在对象被回收时调用,但它的执行时机不可预测(尤其在循环引用或异常时),因此不推荐依赖它来释放资源,应使用上下文管理器(with 语句)。
三、循环引用:引用计数的“死穴”
3.1 问题场景
当两个或多个对象互相引用时,即使外部没有任何变量指向它们,引用计数也永远不会降为 0,导致内存泄漏。
class node:
def __init__(self, name):
self.name = name
self.parent = none
self.child = none
a = node("a")
b = node("b")
a.child = b
b.parent = a
del a
del b
# 此时两个对象引用计数均为 1(互相指向),无法被引用计数机制回收3.2 循环引用的常见形式
- 双向链表/树结构:父子节点互相持有引用
- 闭包:函数内部引用外部变量,外部变量又引用函数
- 回调/观察者模式:对象注册回调,回调又持有对象引用
- 异常栈帧:异常对象持有 traceback,traceback 持有局部变量
四、分代垃圾回收:解决循环引用的利器
4.1 分代回收原理
python 的垃圾回收器(gc 模块)专门处理循环引用。它采用**分代(generational)**策略,将对象分为三代:
- 第 0 代:新创建的对象
- 第 1 代:经历一次 gc 后存活的对象
- 第 2 代:经历多次 gc 后存活的对象
gc 触发频率:第 0 代 > 第 1 代 > 第 2 代。因为大部分对象生命周期很短(如临时变量),频繁扫描年轻代效率更高。
4.2 回收流程(以第 0 代为例)
- 标记(mark):从根对象(全局变量、栈帧中的变量等)出发,遍历所有可达对象,标记为“存活”。
- 清除(sweep):遍历第 0 代所有对象,将未被标记的对象(即不可达的循环引用对象)回收。
- 晋升(promote):本轮存活的对象移入第 1 代。
4.3 阈值与自动触发
import gc print(gc.get_threshold()) # 默认 (700, 10, 10) # 含义:第 0 代对象数量超过 700 时触发第 0 代 gc; # 第 0 代 gc 每进行 10 次,触发一次第 1 代 gc; # 第 1 代 gc 每进行 10 次,触发一次第 2 代 gc。
五、gc模块:手动控制与调试
5.1 常用函数
| 函数 | 作用 |
|---|---|
gc.enable() / gc.disable() | 启用/禁用自动垃圾回收 |
gc.collect([generation]) | 手动执行回收,返回回收的对象数量 |
gc.get_objects() | 返回当前所有被 gc 跟踪的对象列表 |
gc.get_referrers(obj) | 返回所有引用 obj 的对象 |
gc.get_referents(obj) | 返回 obj 引用的所有对象 |
gc.set_debug(flags) | 设置调试标志,如 gc.debug_leak |
5.2 手动回收示例
import gc
# 关闭自动回收,模拟高负载场景
gc.disable()
# 创建循环引用
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, b
# 手动触发回收
unreachable = gc.collect()
print(f"回收了 {unreachable} 个不可达对象") # 输出 25.3 调试内存泄漏
import gc
gc.set_debug(gc.debug_leak) # 打印所有无法回收的对象
# 创建循环引用且包含 __del__ 方法(导致无法回收)
class leak:
def __del__(self):
pass
x = leak()
y = leak()
x.ref = y
y.ref = x
del x, y
gc.collect() # 控制台会输出警告:uncollectable objects六、内存泄漏场景与排查实战
6.1 常见泄漏场景
- 全局缓存未清理:使用
list或dict作为全局缓存,不断添加数据但从不删除。 - 闭包持有大对象:函数内部定义函数,内部函数引用了外部函数的局部变量(如大列表),导致外部函数返回后变量无法释放。
__del__与循环引用:循环引用中的对象定义了__del__方法,gc 无法回收(因为无法确定销毁顺序)。- c 扩展模块:使用
ctypes或cython时,手动分配的内存未释放。 - 线程与
threading.local:线程局部变量在线程结束后未清理。 matplotlib等 gui 库:未关闭的 figure 对象持续占用内存。
6.2 排查工具与方法
方法一:gc.get_objects()统计对象数量
import gc
# 获取所有对象,按类型统计
obj_counts = {}
for obj in gc.get_objects():
t = type(obj).__name__
obj_counts[t] = obj_counts.get(t, 0) + 1
# 排序输出前 20 个
sorted_counts = sorted(obj_counts.items(), key=lambda x: x, reverse=true)
for name, count in sorted_counts[:20]:
print(f"{name}: {count}")方法二:objgraph库(第三方)
pip install objgraph
import objgraph # 显示增长最多的对象 objgraph.show_growth(limit=10) # 生成引用关系图(需要 graphviz) objgraph.show_backrefs([my_object], filename="backrefs.png")
方法三:tracemalloc模块(python 3.4+)
import tracemalloc
tracemalloc.start()
# 运行你的代码...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)方法四:memory_profiler逐行分析
pip install memory_profiler
@profile def my_func(): # 你的代码 pass
运行:python -m memory_profiler script.py
6.3 实战案例:修复一个泄漏
问题:一个 web 服务运行几天后内存持续增长,最终 oom。
排查步骤:
- 使用
gc.get_objects()发现dict对象数量异常多。 - 通过
objgraph.show_growth()发现requestcontext对象持续增加。 - 使用
gc.get_referrers()找到持有这些对象的根——一个全局的list缓存。 - 定位到代码中未清理的请求日志缓存,添加定期清理逻辑。
修复:将全局 list 改为 collections.deque(maxlen=1000),或使用 weakref.weakvaluedictionary。
七、最佳实践总结
| 场景 | 建议 |
|---|---|
| 避免循环引用 | 使用 weakref 模块(弱引用)打破循环 |
| 资源管理 | 使用 with 语句(上下文管理器)而非 __del__ |
| 大对象缓存 | 设置上限(如 lru 缓存)或使用弱引用 |
| 长期运行服务 | 定期调用 gc.collect(2) 强制回收老年代 |
| 调试阶段 | 开启 gc.debug_leak 或使用 tracemalloc |
| 生产环境 | 监控 gc.get_count() 和 gc.get_stats() 指标 |
八、结语
python 的内存管理看似“黑盒”,但通过理解引用计数、分代回收和 gc 模块,我们完全可以掌控内存的来龙去脉。记住:自动回收不是万能药,良好的编码习惯才是内存安全的根本。希望本文能帮你写出更健壮、更高效的 python 代码。
到此这篇关于python内存管理与垃圾回收从原理到实战排查的文章就介绍到这了,更多相关python内存管理与垃圾回收内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论