Python内存泄漏测试

1、 Python内存泄漏处理机制

       为了解决内存泄漏的问题,Python2.0的版本开始引入“引用计数”,并基于引用计数实现了自动垃圾收集,后来为了解决循环引用导致内存泄漏的问题,又引入“标记-清除”、“分代回收”机制。

比如为了提高效率,垃圾收集器被开发人员关闭等情况。

        python提供了扩展模块gc,该模块提供了对该垃圾收集器的操作接口,通过该模块,可以查看收集器状态,调节收集频率,设置一些调试选项,收集垃圾对象,查看垃圾对象详细信息等。

 

2、Python垃圾回收原理

        Python解析器中垃圾回收机制主要由以下三种:

2.1 引用计数

在Python中,如果一个对象的引用数为0,Python就会尝试回收这个对象的内存这使得python垃圾回收具有较高的实时性。可通过sys.getrefcount(object)查看对象引用次数。

导致对象引用计数增加的情况分为以下几种情况:

  1. 象被创建,例如a=23
  2. 对象被引用,例如b=a
  3. 对象被作为参数,传入到一个函数中,例如func(a)
  4. 对象作为一个元素,存储在容器中,例如list1=[a,a]

导致对象引用计数减少有如下几种情况:

  1. 对象的别名被显式销毁,例如:del a
  2. 对象的别名被赋予新的对象,例:a=24
  3. 一个对象离开它的作用域,

例如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.手动进行垃圾对象收集,分析收集结果。

python检查内存泄漏 python 内存泄漏分析工具_python检查内存泄漏

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的例子中已经做了验证,发现局部变量虽然被赋值给全局变量,但是在函数作用域外,局部变量已经被释放掉。