一、问题背景
在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
import objgraph
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_LEAK)
print("Before calling urlopen(), the unreachable object len: %s" % len(gc.garbage))
f = urlopen('http://www.google.com')
del f
# check memory on memory leaks
print("After calling urlopen(), collecting the unreachable objects: %s" % gc.collect())
#import pdb;pdb.set_trace()
输出结果如下所示,从这里看可以发现有7个不可达对象被gc
回收掉。
Before calling urlopen(), the unreachable object len: 0
gc: collectable <HTTPResponse instance at 0x7f7393427820>
gc: collectable <dict 0x7f739342a830>
gc: collectable <HTTPMessage instance at 0x7f7393427910>
gc: collectable <dict 0x7f739342a5f0>
gc: collectable <list 0x7f7393475d70>
gc: collectable <list 0x7f7393d27c80>
gc: collectable <instancemethod 0x7f7393d17b90>
After calling urlopen(), collecting the unreachable objects: 7
从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
三、参考文献
- urllib2's urlopen() method causes a memory leak
- 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