即使在有垃圾回收的语言里,内存泄漏依然可能发生。Python 的内存泄漏通常不是操作系统级别的物理泄露,而是对象生命周期管理失误,导致不再使用的对象仍然被引用,无法被回收。下面梳理最常见的几种场景和对应的排查方法。
常见场景
1. 循环引用中的 del 陷阱
当两个或多个对象相互引用,且其中一个类定义了 del 方法,垃圾回收器会将其放入 gc.garbage 列表而无法自动回收。
import gc
class A:
def __del__(self):
pass
class B:
def __del__(self):
pass
a = A()
b = B()
a.b = b
b.a = a
del a, b
gc.collect()
print(gc.garbage) # 对象不会被彻底清理,会滞留在 gc.garbage 中
对策:尽量避免自定义 del,改用 weakref 或上下文管理器。
2. 全局容器持续增长
无意中向全局列表、字典或类属性中持续添加数据,而又从不清理,导致内存无限增长。
cache = {}
def process_request(req_id):
data = fetch_large_data(req_id)
cache[req_id] = data # 请求结束后没有删除,cache 越来越大
对策:使用 functools.lru_cache 限容、定期清理,或使用 WeakValueDictionary 允许多余条目被回收。
3. 缓存未设置上限
使用 lru_cache 但没有指定 maxsize,或手动实现的缓存字典无限增长。
from functools import lru_cache
@lru_cache(maxsize=None) # 危险:所有结果都会缓存,内存可能暴涨
def expensive(n):
return n * n
对策:设置合理的 maxsize,如 maxsize=128。
4. 循环引用 + 异常导致引用链断裂不全
对象之间循环引用,某个引用被异常打断,导致整个图无法计数归零,分代回收也可能因阈值未到而延迟处理。
class Node:
def __init__(self):
self.parent = None
self.children = []
root = Node()
child = Node()
root.children.append(child)
child.parent = root
# 删除 root 但 child.parent 仍活着,构成循环引用
del root
# 此时 child 和 root 都不可达,但引用计数不为0,需等垃圾回收扫描
对策:显式使用 gc.collect(),或使用弱引用 weakref.ref 打破循环。
5. 外部资源未正确关闭
打开的文件、网络连接、数据库游标等如果没有显式关闭,可能被一直保留在内存中(同时占用系统句柄)。
files = []
for i in range(10000):
f = open('data.txt', 'r')
files.append(f) # 忘记 f.close(),且一直被列表引用
对策:始终使用 with 语句管理资源。
6. C 扩展模块的内部泄漏
某些 C 扩展库(如使用 Cython 手动管理内存的模块)可能忘记释放分配的内存,Python 的垃圾回收无法介入。这类泄漏通常表现为进程内存持续增长,而 Python 对象计数正常。
对策:关注第三方库版本,遇到疑似问题使用 Valgrind(Linux)等工具检测 C 层的泄漏。
排查方法
1. tracemalloc — 内置内存追踪利器
Python 3.4+ 自带,能记录每次内存分配的调用栈,对比“快照”找出持续增长的分配点。
import tracemalloc
tracemalloc.start()
# ... 执行怀疑泄漏的代码段 ...
snapshot1 = tracemalloc.take_snapshot()
# ... 继续执行 ...
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
for stat in top_stats[:10]:
print(stat)
输出会显示内存占用差异最大的文件和行号,快速定位泄漏源头。
2. gc 模块 — 分析循环引用
import gc
gc.set_debug(gc.DEBUG_SAVEALL) # 将所有不可回收对象存入 gc.garbage
# ... 代码 ...
gc.collect()
for obj in gc.garbage:
print(type(obj), obj)
结合 gc.get_objects() 可以列出所有已知 Python 对象,按类型统计,寻找异常庞大的对象群。
from collections import Counter
cnt = Counter(type(o) for o in gc.get_objects())
print(cnt.most_common(5))
3. objgraph — 可视化对象引用链
第三方库 objgraph 可以展示某个类型对象的增长情况,并绘制反引用图(哪些对象在引用它)。
import objgraph
# 假设反复调用某函数后
objgraph.show_growth(limit=5) # 显示增长最快的5种对象
# 如果需要具体到某个实例的引用链
roots = objgraph.get_leaking_objects() # 需先调用 objgraph.show_most_common_types
# objgraph.show_refs([obj], filename='refs.png') # 生成引用图
4. memory_profiler — 逐行内存分析
memory_profiler 可以精确到每一行代码的内存变化。
from memory_profiler import profile
@profile
def leaky_func():
a = [1] * (10**6)
b = [2] * (2 * 10**6)
del b
return a
if __name__ == '__main__':
leaky_func()
运行 python -m memory_profiler example.py 会输出每行的内存增减,直观看出哪里额外占用了内存。
5. 综合排查策略建议
- 先看整体:使用
tracemalloc对比快照,定位到疑似泄漏的文件/函数。 - 再看细节:用
memory_profiler分析可疑函数内部的逐行内存变化。 - 分析对象图:如果局部变量都已消失但内存仍然不降,用
gc和objgraph检查循环引用和意外全局引用。 - 检查 C 扩展:如果 Python 层面一切正常但进程 RSS 持续上涨,在 Linux 下可用
valgrind --tool=memcheck或直接切换库的版本/使用纯 Python 替代测试。
做到这些,多数日常工作中的内存泄漏都能被快速定位和解决。