一、问题背景
在python2/3中调用urllib模块会有循环引用没被释放,触发代码如下所示:
import gc
import sys
if sys.version_info.major == 3:
from urllib import request
urlopen = request.urlopen
else:
import urllib2
urlopen = urllib2.urlopen
gc.collect()
# the unreachable object will be freed if this flag not set
gc.set_debug(gc.DEBUG_SAVEALL)
print("before call urlopen(), the unreachable object len: %s" % len(gc.garbage))
a = urlopen('http://www.google.com')
del a
# check memory on memory leaks
gc.collect()
print("after call urlopen(), the unreachable object len: %s" % len(gc.garbage))
在python2.7执行示例代码可以看到如下输出:
before call urlopen(), the unreachable object len: 0
after call urlopen(), the unreachable object len: 21
在python3.9执行示例代码可以看到如下输出:
before call urlopen(), the unreachable object len: 0
after call urlopen(), the unreachable object len: 0
从触发用例输出结果初步看到python2.7的urllib2模块有内存泄露问题。
二、urllib2模块代码分析
修改测试代码对引用泄露进行分析,修改后的测试代码如下所示:
import gc
import pprint
import sys
if sys.version_info.major == 3:
from urllib import request
urlopen = request.urlopen
else:
import urllib2
urlopen = urllib2.urlopen
gc.collect()
# DEBUG_SAVEALL标志:所有的unreachable objects都会被保存到garbage中,而不是被释放
gc.set_debug(gc.DEBUG_SAVEALL)
print("Before calling urlopen(), the unreachable object len: %s" % len(gc.garbage))
f = urlopen('http://www.baidu.com')
del f
# check memory on memory leaks
gc.collect()
print("After calling urlopen(), the unreachable objects:")
print(pprint.pprint(gc.garbage))
输出结果如下所示,从这里看可以发现有7个不可达对象被gc
回收掉。
Before calling urlopen(), the unreachable object len: 0
After calling urlopen(), the unreachable objects:
[<httplib.HTTPResponse instance at 0x7fa5260efd20>,
{'_method': 'GET',
'chunk_left': 'UNKNOWN',
'chunked': 0,
'debuglevel': 0,
'fp': None,
'length': 29506,
'msg': <httplib.HTTPMessage instance at 0x7fa5260ef3c0>,
'reason': 'OK',
'recv': <bound method HTTPResponse.read of <httplib.HTTPResponse instance at 0x7fa5260efd20>>,
...
从gc
的debug
信息可以看出明显属于urllib
模块的类是HTTPResponse
和HTTPMessage
。此时我们可以将此测试代码最后一行打上断点,通过objgraph
模块将不可达对象的拓扑图打印出来,执行语句:
objgraph.show_refs(objgraph.at(0x7f7393427820), filename="20230225 0x7f7393427820.png", max_depth=5, too_many=30)
由于对象的属性可能非常复杂,所以我们需要调整objgraph.show_refs()
函数中的max_depth
和too_many
参数来不断扩展对象拓扑结构来找循环引用。这里不建议一开始就将max_depth
和too_many
调的很大,可能会导致图片渲染时间较长,另外数据太多会看不清楚依赖细节。0x7f7393427820
对象生成的完整图片如下所示(博客中无法放大,需要把图片才能缩放看到详细细节,补充了一个细节截图):
从这个全局图里面我们可以看见确实有一个循环引用:
在Python.2.7.18模块库中搜索
urllib2
、httplib
等相关模块可以发现,只有此截图处(在urllib2.py中的AbstractHTTPHandler
类中)有recv
属性的定义。
实际这行赋值定义操作移至上层基类
HTTPResponse
中定义就不会有循环引用问题。在HTTPResponse.read()
函数后面并行的添加如下代码:
class HTTPResponse:
def read(self, amt=None):
#...
recv = read
然后重新执行测试用例会发现循环引用消失了。
Before calling urlopen(), the unreachable object len: 0
After calling urlopen(), collecting the unreachable objects: 0
三、扩展总结
Python代码内存泄露只会有两种情况:
1、对象资源的引用计数未清零:a、打开文件、连接等资源忘记关闭,而且是多次循环执行;b、对象间出现循环引用,GC无法释放不可达对象;
2、GC被人为或者系统关闭;
引用计数未清零
这段代码文件资源被打开后未被关闭,而且批量进行了文件的打开和写操作,这会导致内存泄露。但这段代码无法用gc.collect()
及gc.garbage
检查得出,因为这段代码虽然内存泄露了,但资源对象还是可以达到的,在相关函数结束后可以正常被gc回收。
def write_log():
f = open('log.txt', 'w')
f.write('something happened')
for _ in range(10):
write_log()
GC被人为或者系统关闭
在python2.7.6版本里面,使用subprocess
模块可能会导致GC被关闭 bpo-1336。
可以先用tracemalloc
模块看代码内存的分配统计信息,再用gc
模块或者objgraph
工具对对象引用进一步统计分析。
四、参考文献
- urllib2's urlopen() method causes a memory leak
- 记一个python默认参数引用泄露问题
- 记一个python异常返回导致内存泄露问题
- https://www.cnblogs.com/anjike/p/10230302.html
- urlopen内存泄漏浅析
- objgraph: Memory leak example
- Why disable the garbage collector?
- subprocess.Popen hangs when child writes to stderr
- tracemalloc模块使用说明