下面是再现内存泄漏的程序,运行此程序之后,可以观察到进程的内存使用量一直上升。
# 创建一个测试用的文件
X = np.random.randn(1000,1000)
np.savez('tmp.npz',X=X)
# 循环重复载入此文件
for i in xrange(1000000):
data = np.load('tmp.npz')
data.close()
为了弄清楚是什么对象没有被垃圾回收,我们可以在循环多次之后,调用gc模块的get_objects()获得当前内存中的所有对象,并对这些对象的类型进行计数统计。下面是这部分程序:
data = np.load('tmp.npz')
data.close()
import gc
from collections import defaultdict
d = defaultdict(int) #使用defaultdict对对象进行计数
for o in gc.get_objects():
name = type(o).__name__ #获得对象的类型名
d[name] += 1
items = d.items()
items.sort(key=lambda x:x[1])
for key, value in items:
print key, value
这段程序使用defaultdict对内存中的对象进行计数,键为对象的类型名,值为对象的个数。最后我们按照个数从小到大排列并输出。下面是对象个数最多的几种类型:
function 2416
tuple 9440
BagObj 10000
NpzFile 10000
list 20297
dict 21032
由于程序循环了10000次,显然BagObj和NpzFile由load()产生,但是未能被垃圾回收的对象。
知道了产生泄漏的类名,我们可以在NumPy的安装文件夹下搜索着两个字符串,在我的电脑上找到:
分析此文件中的BagObj和NpzFile的定义:
def __init__(self, fid, own_fid=False):
...
self.zip = _zip
self.f = BagObj(self) ❶
if own_fid:
self.fid = fid
else:
self.fid = None
def close(self):
"""
Close the file.
"""
if self.zip is not None:
self.zip.close()
self.zip = None
if self.fid is not None:
self.fid.close()
self.fid = None
def __del__(self): ❷
self.close()
class BagObj(object):
def __init__(self, obj):
self._obj = obj ❸
def __getattribute__(self, key):
try:
return object.__getattribute__(self, '_obj')[key]
except KeyError:
raise AttributeError, key
❷我们发现NpzFile类定义了__del__(),有此方法的类特别需要注意内存泄漏问题。❶NpzFile的f属性是一个BagObj对象,❸而此BagObj对象的_obj属性是NpzFile对象。这样在NpzFile和BagObj对象之间存在循环引用。
在gc模块的文档中特别提到过循环引用并存在__del__()时的内存释放问题:
为了解决这个问题,我们需要打破循环引用。对于本问题来说,有两种方法:
data = np.load('tmp.npz')
del data.f ❶
#del data.f._obj ❷
data.close()
❶删除NpzFile对BagObj的引用。这时BagObj对象将会被先回收,从而使得NpzFile对象的引用次数为0,调用其__del__(),并回收NpzFile对象。
❷删除BagObj对NpzFile的引用。这时NpzFile对象的引用次数为0,调用其__del__(),回收NpzFile对象之后,BagObj对象的引用次数变为0,回收。
从上面的分析可知,根据打破循环引用的方式不同,__del__()调用的时刻也不同。当循环引用中的各个对象没有__del__()时,对象回收的顺序不会造成任何影响。如果有对象包含__del__(),垃圾回收器就犯难了,它无法擅自决定__del__()的调用时刻,于是它选择了让这些对象始终在内存中呆着。