Python 内存泄漏检测
内存泄漏是指由于程序运行过程中,某些对象没有正常释放,成为了常驻在内存的幽灵。随着程序的运行,这样的对象越来越多,直到占满整个内存,导致程序退出。
在Python中,由于垃圾回收机制的存在,它的内存分配不像C/C++一样需要人为的分配和回收,很少会出现内存泄漏的问题。但是也存在一些场景,垃圾回收机制无法对已分配的内存进行回收,而这就带来了内存泄漏的问题。
内存泄漏的主要场景
Python中的垃圾回收使用的是引用计数,所以首先需要理解什么是引用。所有的赋值操作都会增加一个引用,如下面的例子所示,b和c都有a的引用。
>>> a = 1
>>> b = []
>>> b.append(1)
>>> c = a
当一个对象引用另一个对象时,被引用对象的计数会+1;当这个引用被删除时,被引用对象的计数会-1;当对某个对象的引用变为0的时候,垃圾回收器就会自动的将这个对象销毁掉。
在Python中,内存泄漏主要有下面两种情况:
- 对象被某全局对象所引用,程序运行期间无法释放
- 对象间存在循环引用,且引用链中某个对象定义了
__del__
方法,导致垃圾回收器无法确定回收的顺序(普通的循环引用垃圾回收器可以自动的进行处理)
内存泄漏的检测工具
在尝试了许多检测工具之后,推荐使用下面的工具对内存泄漏进行检测。
Pyrasite
首先是 Pyrasite,它是一个内存注入工具,通过绑定到指定Python进程,可以对程序的运行状态进行监控。非注入式的工具通常通过在原程序中添加检测代码段来对内存运行情况进行监测,但是当代码量过大无法确定监测点时很难快速定位问题,而Pyrasite可以在不修改源码的情况下完成这一操作。Pyrasite一共包括了四个工具:
- pyrasite, 程序注入工具,以运行脚本的形式注入
- pyrasite-shell, 创建一个绑定到指定PID的SHELL
- pyrasite-memory-viewer, 查看指定进程的对象内存占用情况
- pyrasite-gui,图形界面版的Pyrasite
其中pyrasite-shell最为常用,可以通过运行如下命令打开一个Python命令行,命令行会绑定到指定Python进程,可以对该进程进行调试。
pyrasite-shell pid
gc
Python标准库中的 gc 模块,可以通过这个模块对Python内存回收器进行管理,常用的方法如下:
gc.collect() # 手动运行垃圾回收
gc.garbage # 无法自动回收的对象列表
gc.set_debug(flags) # 设置DEBUG标识,方便进行调试
gc.get_referrers(*objs) # 获得某个对象的引用对象列表
objgraph
objgraph 是一个python对象关系可视化工具,API由三部分组成:
- 对象统计
- 对象选择器
- 可视化
对象统计最常用的方法是objgraph.show_most_common_types
和objgraph.show_growth
。前者打印当前进程各类对象的数量,后者每次调用会统计从上次调用到当前调用各类对象数的增量。需要注意的是它并不会统计int或str这些简单类型
>>> import objgraph
>>> objgraph.show_most_common_types()
function 1159
wrapper_descriptor 1039
builtin_function_or_method 704
method_descriptor 541
tuple 534
dict 482
weakref 339
list 199
member_descriptor 187
getset_descriptor 167
# 第一次调用增量等于当前对象数量
>>> objgraph.show_growth()
function 1159 +1159
wrapper_descriptor 1047 +1047
builtin_function_or_method 704 +704
method_descriptor 541 +541
dict 473 +473
weakref 341 +341
tuple 240 +240
list 199 +199
member_descriptor 191 +191
getset_descriptor 169 +169
# 创建了1个list对象,可以看到show_growth的list对象也增加了1个
>>> a = [1,2,3]
>>> objgraph.show_growth()
list 200 +1
对象选择器可以获取指定对象,例如objgraph.by_type可以获得指定类型对象列表,objgraph.at可以获得指定id的对象。
>>> class B():
... pass
...
>>> b = B()
# 通过类型获得class B对象
>>> objgraph.by_type('B')
[<__main__.B instance at 0x7f213cd97098>]
# 通过ID获得指定对象
>>> id(b)
139780731531416
>>> objgraph.at(139780731531416)
<__main__.B instance at 0x7f213cd97098>
最后是最重要的可视化部分,对于寻找内存泄漏,最重要的就是查看对象引用链,但是随着程序规模的增长,一个程序里会有数万个对象,很难直接查看对象间的引用,而objgraph通过将对象引用绘制为图的形式,极大的简化了人工筛选的难度。
>>> c = [b,]
>>> objgraph.show_backrefs(objgraph.by_type('B'), filename='b.png')
Graph written to /tmp/objgraph-6f9iow.dot (9 nodes)
Image generated as b.png
程序会输出这样一幅图,可以看到有两个对象持有对b的引用,一个是b,一个是c。
一个内存泄漏的例子
下面是一个简单的循环引用带来的内存泄漏程序
# demo.py
class A(object):
def __init__(self):
self._b = B(self)
def __del__(self):
return
class B(object):
def __init__(self, a):
self._a = a
if __name__ == '__main__':
import time
while True:
a = A()
time.sleep(1)
现在发现运行时内存会持续增加,所以需要确定内存泄漏点,并进行修复。首先查看进程的ID
>>> ps -ef | grep python
得到结果如下,进程ID为6253
然后运行pyrasite-shell,绑定到当前进程
>>> pyrasite-shell 6253
运行结果大致如下
接下来需要确定引起泄漏的原因
>>> import objgraph
# 先执行一次show_growth得到基准
>>> objgraph.show_growth()
function 1327 +1327
wrapper_descriptor 1050 +1050
builtin_function_or_method 724 +724
dict 592 +592
method_descriptor 589 +589
tuple 484 +484
weakref 478 +478
list 236 +236
getset_descriptor 211 +211
member_descriptor 210 +210
# 等待一段时间之后再次执行
>>> objgraph.show_growth()
dict 691 +94
A 74 +47
B 74 +47
可以看到A,B对象都在不断增长中,说明这个两个对象引起了泄漏(通常dict,list之类的对象由于大量存在,不需要优先考虑,除非没有找到自定义的对象)
找到泄漏的对象后,再查看一下它的引用情况
>>> objgraph.show_chain(
... objgraph.find_backref_chain(
... objgraph.by_type('A')[-1],
... objgraph.is_proper_module),
... filename='chain.png')
Graph written to /tmp/objgraph-Q3T08P.dot (4 nodes)
Image generated as chain.png
最后生成的图如下,可以看到最下面标出了红色的__del__
方法,说明这个对象无法被gc模块自动回收