Python内存泄漏

一、Python内存管理

    Python中,python中,一切都是对象,又分为mutable和immutable对象。二者区分的标准在于是否可以原地修改,“原地”可以理解为相同的地址。可以通过id()查看一个对象的“地址”,如果通过变量修改对象的值,但id没发生变化,那么就是mutable,否则就是immutable。比如:

#immutable
a=1
print(id(a))#140734991946784
a=3
print(id(a))#140734991946848

#mutable
b =[1,2,3]
print(id(b))#1273753854536
b.append(4)
print(id(b))#1273753854536

1、引用计数

引用计数(References count),指的是每个Python对象都有一个计数器,记录着当前有多少个变量指向这个对象。

通过sys.getrefcount(obj)对象可以获得一个对象的引用数目,返回值是真实引用数目加1(加1的原因是obj被当做参数传入了getrefcount函数),例如:

import sys
    s = 1
    print(sys.getrefcount(s))#1297
    a = 'jkl'
    print(sys.getrefcount(a))#4

从中可以看出,Python中的对象缓存池会缓存十分常用的immutable对象,比如整数1。

2、垃圾回收

    这里强调一下,本文中的的垃圾回收是狭义的垃圾回收,是指当出现循环引用,引用计数无计可施的时候采取的垃圾清理算法。

在python中,使用标记-清除算法(mark-sweep)和分代(generational)算法来垃圾回收

3、gc module

这里的gc(garbage collector)是Python 标准库,该module提供了与上一节“垃圾回收”内容相对应的接口。通过这个module,可以开关gc、调整垃圾回收的频率、输出调试信息。gc模块是很多其他模块(比如objgraph)封装的基础,在这里先介绍gc的核心API。

import gc

gc.enable()#开启gc(默认开启)

gc.disable()#关闭gc

gc.isenabled()#判断gc是否开启

gc.collect()#执行一次垃圾回收,不管gc是否处于开启状态都能使用

gc.set_threshold(t0,t1,t2)#设置垃圾回收阈值

gc.get_threshold()# 获得当前的垃圾回收阈值

gc.get_objects()#返回所有被垃圾回收器(collector)管理的对象

gc.get_referents(*obj)#返回obj对象直接指向的对象

gc.get_referrers(*obj)#返回所有直接指向obj的对象

gc.set_debug(gc.DEBUG_COLLECTABLE)#打印可以被垃圾回收器回收的对象

gc.set_debug(gc.DEBUG_UNCOLLECTABLE)#打印无法被垃圾回收器回收的对象,即定义了__del__的对象

gc.set_debug(gc.DEBUG_SAVEALL)#当设置了这个选项,可以被拉起回收的对象不会被真正销毁(free),而是放到gc.garbage这个列表里面,利于在线上查找问题

二、内存泄漏

内存泄漏产生的两种情况:

1、是对象被另一个生命周期特别长的对象所引用。

解决方式:只要在适当的时机解除引用。

2、是循环引用中的对象定义了__del__函数,如果定义了__del__函数,那么Python中的解析器就

不能判断解析对象,所以不会做处理。

解决方法:要么不再使用__del__函数,换一种实现方式,要么解决循环引用。

查找内存泄漏的两个库:gc、objgraph

1、objgraph

objgraph的实现调用了gc的这几个函数:gc.get_objects(), gc.get_referents(), gc.get_referers(),然后构造出对象之间的引用关系。

常用的方法:

1、def count(typename):返回该类型对象的数目,其实就是通过gc.get_objects()拿到所用的对象,然后统计指定类型的数目。

2、def by_type(typename):返回该类型的对象列表。线上项目,可以用这个函数很方便找到一个单例对象。

3、def show_most_common_types(limits = 10):打印实例最多的前N(limits)个对象,这个函数非常有用。在《Python内存优化》一文中也提到,该函数能发现可以用slots进行内存优化的对象。

3、def show_growth():统计自上次调用以来增加得最多的对象,这个函数非常有利于发现潜在的内存泄露。函数内部调用了gc.collect(),因此即使有循环引用也不会对判断造成影响。

4、def show_backrefs():生产一张有关objs的引用图,看出看出对象为什么不释放,后面会利用这个API来查内存泄露

5、def find_backref_chain(obj, predicate, max_depth=20, extra_ignore=()):找到一条指向obj对象的最短路径,且路径的头部节点需要满足predicate函数 (返回值为True),可以快捷、清晰指出 对象的被引用的情况,后面会展示这个函数的威力

6、def show_chain():将find_backref_chain 找到的路径画出来, 该函数事实上调用show_backrefs,只是排除了所有不在路径中的节点。

2、查找内存泄漏

查找方法,先调用一次objgraph.show_growth(),然后调用怀疑内存泄漏的函数,最后再调用一次objgraph.show_growth(),看看是否有增加对象。

# -*- coding: utf-8 -*-
import objgraph
da= []
class a():
    pass

def run():
    b=a()
    da.append(b)
    if True:
        return
    da.remove(b)

if __name__ == '__main__':
    objgraph.show_growth()
    try:
        run()
    except:
        pass
    objgraph.show_growth()

# 显示
# function                       2150     +2150
# wrapper_descriptor             1096     +1096
# dict                           1037     +1037
# tuple                           851      +851
# builtin_function_or_method      763      +763
# method_descriptor               753      +753
# weakref                         711      +711
# getset_descriptor               393      +393
# member_descriptor               307      +307
# type                            306      +306
# a        1        +1

三、循环引用

如果存在循环引用,那么Python的gc就必须开启(gc.isenabled()返回True),否则就会内存泄露。

1、    定位循环引用

这里还是是用GC模块和objgraph来定位循环引用。需要注意的事,一定要先禁用gc(调用gc.disable()), 防止误差。

这里利用之前介绍循环引用时使用过的例子: a, b两个OBJ对象形成循环引用

import objgraph,gc

class A(object):
    pass

def run():
    a,b = A(),A()
    a.attr_b = b
    b.attr_a = a

if __name__ == '__main__':
    gc.disable()
    for x in range(50):
        run()
    objgraph.show_most_common_types(20)

# function                   2150
# dict                       1125
# wrapper_descriptor         1092
# builtin_function_or_method 762
# method_descriptor          752
# tuple                      670
# weakref                    597
# getset_descriptor          375
# member_descriptor          307
# type                       193
# cell                       173
# list                       152
# module                     112
# ModuleSpec                 110
# A                          100
# classmethod                86
# set                        79
# _NamedIntConstant          74
# frozenset                  72
# SourceFileLoader           70

在实际项目中,不大可能到处用objgraph.show_most_common_types或者objgraph.by_type来排查循环引用,效率太低。有没有更好的办法呢,有的,那就是使用gc模块的debug 选项。在前面介绍gc模块的时候,就介绍了 # gc: collectable # gc: collectable # gc: collectable " v:shapes="_x0000_s1029">gc.DEBUG_COLLECTABLE 选项,我们来试试。

import gc,time

class A(object):
    pass

def run():
    a, b = A(), A()
    a.attr_b = b
    b.attr_a = a

if __name__ == '__main__':
    gc.disable()
    gc.set_debug(gc.DEBUG_COLLECTABLE)
    for x in range(1):
        run()
    gc.collect()
    time.sleep(1)

# gc: collectable <A 0x000001BFB9E85A60>
# gc: collectable <A 0x000001BFB9E85A00>
# gc: collectable <dict 0x000001BFB828FFC0>
# gc: collectable <dict 0x000001BFB829F640>

2、消灭循环引用

找到循环引用关系之后,解除循环引用就不是太难的事情,总的来说,有两种办法:

①手动解除与使用weakref。

②手动解除很好理解,就是在合适的时机,解除引用关系

常用的方法:

(1)weakref.ref(object, callback = None)

创建一个对object的弱引用,返回值为weakref对象,callback: 当object被删除的时候,会调用callback函数,在标准库logging (__init__.py)中有使用范例。使用的时候要用()解引用,如果referant已经被删除,那么返回None。比如下面的例子

import weakref
class A(object):
    def f(self):
        print("asd")

if __name__ == '__main__':
    a=A()
    w = weakref.ref(a)
    w().f()
    del a
    w().f()

运行上面的代码,会抛出异常:AttributeError: 'NoneType' object has no attribute 'f'。因为这个时候被引用的对象已经被删除了

(2)weakref.proxy(object, callback = None)

创建一个代理,返回值是一个weakproxy对象,callback的作用同上。使用的时候直接用 和object一样,如果object已经被删除 那么跑出异常   ReferenceError: weakly-referenced object no longer exists。

import weakref
class A(object):
    def f(self):
        print("asd")

if __name__ == '__main__':
    a=A()
    w = weakref.proxy(a)
    w.f()
    del a
    w.f()

(3)weakref.WeakSet

  这个是一个弱引用集合,当WeakSet中的元素被回收的时候,会自动从WeakSet中删除。WeakSet的实现使用了weakref.ref,当对象加入WeakSet的时候,使用weakref.ref封装,指定的callback函数就是从WeakSet中删除。感兴趣的话可以直接看源码(_weakrefset.py),下面给出一个参考例子:

import weakref
class A(object):
    def f(self):
        print("asd")

if __name__ == '__main__':
    a=A()
    w = weakref.WeakSet()
    w.add(a)
    print (len(w))# 1
    del a
    print (len(w))# 0

Python中objgraph模块官方文档网址:https://mg.pov.lt/objgraph/

Python中的gc模块官方文档网址:https://docs.python.org/3/library/gc.html

原文地址:https://www.cnblogs.com/dcpb/p/14267499.html