Python内存泄漏测试
1、 Python内存泄漏处理机制
为了解决内存泄漏的问题,Python2.0的版本开始引入“引用计数”,并基于引用计数实现了自动垃圾收集,后来为了解决循环引用导致内存泄漏的问题,又引入“标记-清除”、“分代回收”机制。
Python的垃圾收集器可以让python程序良好运行,但仍有其他原因可能造成python内存泄漏的情况,比如为了提高效率,垃圾收集器被开发人员关闭等情况。
python提供了扩展模块gc,该模块提供了对该垃圾收集器的操作接口,通过该模块,可以查看收集器状态,调节收集频率,设置一些调试选项,收集垃圾对象,查看垃圾对象详细信息等。
2、Python垃圾回收原理
Python解析器中垃圾回收机制主要由以下三种:
2.1 引用计数
在Python中,如果一个对象的引用数为0,Python就会尝试回收这个对象的内存这使得python垃圾回收具有较高的实时性。可通过sys.getrefcount(object)查看对象引用次数。
导致对象引用计数增加的情况分为以下几种情况:
导致对象引用计数减少有如下几种情况:
- 对象的别名被显式销毁,例如:del a
- 对象的别名被赋予新的对象,例:a=24
- 一个对象离开它的作用域,
例如func函数执行完毕时,func函数中定义的局部变量,
对象所在的容器被销毁,或从容器中删除对象
示例:
Global_list = ["hello","candy"]
def test():
temp_list = ["hello","kun"]
Global_list.append(temp_list)
if __name__ == '__main__':
test()
print gc.collect() #打印0
object_nums = len(gc.get_objects())
object_list = gc.get_objects() #获取所有被收集到的垃圾对象
f = open('/root/log', 'w')
i = 0
while True:
f.write(str(object_list[i])) #保存垃圾对象信息到log
i = i + 1
if i == object_nums:
break
f.close()
print len(gc.garbage) #打印:0
print Global_list #打印:['hello', 'candy', ['hello', 'kun']]
结果分析:查看log文件,可以查找到对象temp_list的相关描述,即该局部变量在函数执行完后被垃圾收集器识别并释放。
2.2 标记-清除
为了解决引用计数最致命的缺陷,即无法释放循环引用对象导致的内存泄漏问题,Python引入了“标记-清除”检测机制。当两个对象的引用计数都为1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的,它们的引用计数虽然表现为非0,但实际上有效的引用计数为0,那么被标记到的这两个对象会回收,不存在内存泄漏的情况。如:
示例1:
def Cyclic_reference_test1():
a = classA()
b = classA()
a.t = b
b.t = a
del a #由于循环调用此时a 对象引用次数为1
del b #由于循环调用此时b 对象引用次数为1
print gc.collect() #返回4
print len(gc.garbage) #返回0,该对象已被释放
示例2:
def test_leak():
a = []
a.append(a)
del a
print gc.collect() #返回1
print len(gc.garbage) #返回0,该对象已被释放
示例3:
def test_leak():
a = []
b = []
a.append(b)
b.append(a)
if __name__ == '__main__':
test_leak()
print gc.collect() #返回2
print len(gc.garbage) #返回0,该对象已被释放
2.3 分代回收
当一个对象长期被调用,为了减少收集该对象导致的效率降低问题,Python引入了“分代回收”检测机制。Python根据对象存活的生命周期将内存对象划分为3代,即通过该对象被收集器检测到的次数进行分代,垃圾回收gc.collect()时可以设置回收代(0,1,2),暂时关闭对存活周期长的对象的回收检测,以提高程序的执行效率。gc.collect()不带参数时,默认参数为2,即默认回收3代对象,每一代的垃圾对象回收频率可通过gc.set_threshold()设置。
3 、Python垃圾回收触发条件
默认状态下,Python垃圾回收条件触发后,收集器收集到的垃圾对象会被释放,以下三种情况会触发垃圾回收:
- 手动调用gc.collect()
- 当gc模块的计数器达到阀值的时候。
- 程序功能退出的时候
gc模块常用函数描述
gc.isenabled()
如果当前自动收集器已经打开,返回True.
gc.disable()
关闭自动收集器。此时若产生了不可访问的垃圾对象将不会被释放,会持续占用内存。
gc.collect()
收集垃圾对象,并返回收集器不可访问的垃圾对象个数,同时释放所有收集到的对象并将无法释放的垃圾对象保存至gc.garbage列表中。
gc.set_debug()
设置当前收集器的调试状态,可通过该函数打印经过收集器的所有对象信息,不可访问的对象信息等等,还可以将不可访问的对象保存至gc.garbage列表中(即不释放该对象),以便开发人员定位。
gc.garbage
存放无法释放的对象详细信息,是一个列表,可通过len(gc.garbage)查看造成内存泄漏的对象个数。
该模块其他函数功能可查看python官方文档。
4、内存泄漏示例:
1. 循环引用并定义了__del__方法导致的垃圾对象不可访问且无法被gc释放的例子:
import time,gc
class A():
def __init__(self):
pass
def __del__(self):
pass
def test_leak():
c1 = A()
c2 = A()
c1.t=c2
c2.t=c1
del c1
del c2
if __name__ == '__main__':
test_leak()
print gc.collect() #输出4
print len(gc.garbage) #输出2,有两个垃圾变量未被回收,发生内存泄漏
print gc.garbage
#输出:[<__main__.A instance at 0xb6caf2d8>, <__main__.A instance at 0xb6caf300>]
结果分析:len(gc.garbage) 返回了不可释放对象的个数2,说明有两个对象未被释放,造成程序内存泄漏,再根据详细打印结果可定位至class A,由于c1、c2都定义了__del__方法,导致gc模块无法识别调用__del__的安全次序,所以gc模块无法销毁这些不可访问的对象, gc模块会把对象放到gc.garbage中,但是不会销毁对象。
2. 由于调整了gc状态导致的垃圾对象不可访问且无法释放的情况:
def test_leak():
a = []
b = []
a.append(b)
b.append(a)
if __name__ == '__main__':
gc.set_debug(gc.DEBUG_LEAK)
test_leak()
print gc.collect() #输出2
print len(gc.garbage) #输出2,有两个垃圾变量未被回收,发生内存泄漏
5. Python内存泄漏测试方法
以上的例子虽然可以查看源码是否存在不能被释放的垃圾对象,但由于更改了源码,就测试而言,显然不太理想。为此,需要在gc模块的基础上,结合pyrasite工具,对当前正在运行的程序进行内存泄漏方面的调试。主要分为以下几个步骤:
I.获取进程id并使用pyrasite模块在该进程进行gc调试
II.获取当前gc状态,等待进程运行相关功能模块
III.手动进行垃圾对象收集,分析收集结果。
Figure 1. Test frame diagram
5.1 Test Case实例
下面以正在运行的tpa程序作为例子,通过rpc调用发布“io get”方法,并且是执行成功的调用。
def io_get_success_for_memory_leak_test(args):
sepyrasite = SePyrasite("192.168.1.75")
sepyrasite.pyrasite_init(args[6])
Gcisenabled = 0
DebugState = int(sepyrasite.pyrasite_get_debug())
#检测是否是因为关闭了自动回收机制导致的内存泄漏
if sepyrasite.pyrasite_isenabled() == False:
Gcisenabled = 1
sepyrasite.pyrasite_enable()
sepyrasite.pyrasite_collect()
TempValue = int(sepyrasite.pyrasite_get_garbage_len())
#-------- start add code -------
rpc = TestRpcClient.TestRpcClient('tpa','192.168.1.75')
ret = rpc.call("test.io_get","instrument",["bit1", "bit2"])
print ret
#-------- add code end ---------
value = sepyrasite.pyrasite_collect()
RetValue = int(sepyrasite.pyrasite_get_garbage_len())
if Gcisenabled == 0:
#检测是否是因为执行了操作之后导致的内存泄漏
if TempValue == RetValue:
sepyrasite.pyrasite_exit()
return ["PASS"]
else:
sepyrasite.pyrasite_set_debug()
#-------- start add code -------
rpc.call("test.io_get","instrument",["bit1", "bit2"])
#-------- add code end ---------
sepyrasite.pyrasite_collect()
print "log..........",sepyrasite.pyrasite_garbage()
sepyrasite.pyrasite_set_debug_by_state(DebugState)
sepyrasite.pyrasite_exit()
return ["FAIL"]
else:
if value == 0:
sepyrasite.pyrasite_disable()
epyrasite.pyrasite_exit()
return ["PASS"]
else:
sepyrasite.pyrasite_set_debug()
#-------- start add code -------
rpc.call("test.io_get","instrument",["bit1", "bit2"])
#-------- add code end ---------
sepyrasite.pyrasite_collect()
print "log..........",sepyrasite.pyrasite_garbage()
sepyrasite.pyrasite_set_debug_by_state(DebugState)
sepyrasite.pyrasite_disable()
sepyrasite.pyrasite_exit()
return ["FAIL"]
该测试用例的目的是:正在运行的程序,没有操作时,是阻塞等待状态;只有当有操作时(其他进程通过rpc进行调用操作时),程序才会产生新的变量及对象垃圾,当这个操作结束后,这些新产生的垃圾应该全部被释放,这样才不会发生内存泄漏;反之,如果存在不可被释放的垃圾,我们就将这些对象的内存地址写入日志中,并且判断这个操作后,被测进程发生了内存泄漏。
标黄色部分是我们需要做的操作动作,绿色部分是需要记录的导致内存泄漏的具体垃圾信息,其它则是这个内存泄漏test case的流程代码,一般可以不变,改变只是黄色部分的代码。
Test case可以检测到以下两种内存泄漏的情况:
1. python垃圾回收机制被禁用或者设置成debug状态, 垃圾所占的内存不会被自动释放,从而导致的内存泄漏的情况。
2. 如果用户在自定义的类中实现了__del__方法,并且在实际应用中进行了循环引用,使得python回收机制不能对这些对象垃圾进行回收,从而导致的内存泄漏的情况。
另外:就我们开会中所提到的,全局变量引用局部变量,会不会使得该局部变量不可被释放而导致内存泄漏的问题,我们在2.1的例子中已经做了验证,发现局部变量虽然被赋值给全局变量,但是在函数作用域外,局部变量已经被释放掉。