使⽤gc、objgraph⼲掉python内存泄露与循环引⽤!
  Python使⽤引⽤计数和垃圾回收来做内存管理,前⾯也写过⼀遍⽂章《》,介绍了在python中,如何profile内存使⽤情况,并做出相应的优化。本⽂介绍两个更致命的问题:内存泄露与循环引⽤。内存泄露是让所有程序员都闻风丧胆的问题,轻则导致程序运⾏速度减慢,重则导致程序崩溃;⽽循环引⽤是使⽤了引⽤计数的数据结构、编程语⾔都需要解决的问题。本⽂揭晓这两个问题在python语⾔中是如何存在的,然后试图利⽤gc模块和objgraph来解决这两个问题。
  注意:本⽂的⽬标是Cpython,测试代码都是运⾏在Python2.7。另外,本⽂不考虑C扩展造成的内存泄露,这是另⼀个复杂且头疼的问题。
 本⽂地址:
⼀分钟版本
  (1)python使⽤引⽤计数和垃圾回收来释放(free)Python对象
  (2)引⽤计数的优点是原理简单、将消耗均摊到运⾏时;缺点是⽆法处理循环引⽤
  (3)Python垃圾回收⽤于处理循环引⽤,但是⽆法处理循环引⽤中的对象定义了__del__的情况,⽽且每次回收会造成⼀定的卡顿
  (4)gc module是python垃圾回收机制的接⼝模块,可以通过该module启停垃圾回收、调整回收触发的阈值、设置调试选项
  (5)如果没有禁⽤垃圾回收,那么Python中的内存泄露有两种情况:要么是对象被⽣命周期更长的对象所引⽤,⽐如global作⽤域对象;要么是循环引⽤中存在__del__
  (6)使⽤gc module、objgraph可以定位内存泄露,定位之后,解决很简单
  (7)垃圾回收⽐较耗时,因此在对性能和内存⽐较敏感的场景也是⽆法接受的,如果能解除循环引⽤,就可以禁⽤垃圾回收。
  (8)使⽤gc module的DEBUG选项可以很⽅便的定位循环引⽤,解除循环引⽤的办法要么是⼿动解除,要么是使⽤weakref python内存管理
  Python中,⼀切都是对象,⼜分为mutable和immutable对象。⼆者区分的标准在于是否可以原地修改,“原地“”可以理解为相同的地址。可以通过id()查看⼀个对象的“地址”,如果通过变量修改对象的值,但id没发⽣变化,那么就是mutable,否则就是immutable。⽐如:
>>> a = 5;id(a)
35170056
>>> a = 6;id(a)
35170044
>>> lst = [1,2,3]; id(lst)
39117168
>>> lst.append(4); id(lst)
39117168
  a指向的对象(int类型)就是immutable,赋值语句只是让变量a指向了⼀个新的对象,因为id发⽣了变化。⽽lst指向的对象(list类型)为可变对象,通过⽅法(append)可以修改对象的值,同时保证id⼀致。
  判断两个变量是否相等(值相同)使⽤==,⽽判断两个变量是否指向同⼀个对象使⽤ is。⽐如下⾯a1 a2这两个变量指向的都是空的列表,值相同,但是不是同⼀个对象。
>>> a1, a2 = [], []
>>> a1 == a2
True
>>> a1 is a2
False
  为了避免频繁的申请、释放内存,避免⼤量使⽤的⼩对象的构造析构,python有⼀套⾃⼰的内存管理机制。在巨著《Python源码剖析》中有详细介绍,在python源码obmalloc.h中也有详细的描述。如下所⽰:
  可以看到,python会有⾃⼰的内存缓冲池(layer2)以及对象缓冲池(layer3)。在Linux上运⾏过Python服务器的程序都知道,python 不会⽴即将释放的内存归还给操作系统,这就是内存缓冲池的原因。⽽对于可能被经常使⽤、⽽且是immutable的对象,⽐如较⼩的整数、长度较短的字符串,python会缓存在layer3,避免频繁创建和销毁。例如:
>>> a, b = 1, 1
>>> a is b
True
>>> a, b = (), ()
>>> a is b
True
>>> a, b = {}, {}
>>> a is b
False
  本⽂并不关⼼python是如何管理内存块、如何管理⼩对象,感兴趣的读者可以参考和上的这两篇⽂章。
  本⽂关⼼的是,⼀个普通的对象的⽣命周期,更明确的说,对象是什么时候被释放的。当⼀个对象理论上(或者逻辑上)不再被使⽤了,但事实上没有被释放,那么就存在内存泄露;当⼀个对象事实上已经不可达(unreachable),即不能通过任何变量到这个对象,但这个对象没有⽴即被释放,那么则可能存在循环引⽤。
引⽤计数
  引⽤计数(References count),指的是每个Python对象都有⼀个计数器,记录着当前有多少个变量指向这个对象。
  将⼀个对象直接或者间接赋值给⼀个变量时,对象的计数器会加1;当变量被del删除,或者离开变量所在作⽤域时,对象的引⽤计数器会减1。当计数器归零的时候,代表这个对象再也没有地⽅可能使⽤了,因此可以将对象安全的销毁。Python源码中,通过Py_INCREF和Py_DECREF两个宏来管理对象的引⽤计数,代码在object.h
1#define Py_INCREF(op) (                        \
2    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA      \
3    ((PyObject*)(op))->ob_refcnt++)
4
5#define Py_DECREF(op)                                  \
6    do {                                                \
7if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA      \
8        --((PyObject*)(op))->ob_refcnt != 0)            \
9            _Py_CHECK_REFCNT(op)                        \
10else                                            \
11        _Py_Dealloc((PyObject *)(op));                  \
12    } while (0)
  通过fcount(obj)对象可以获得⼀个对象的引⽤数⽬,返回值是真实引⽤数⽬加1(加1的原因是obj被当做参数传⼊了getrefcount函数),例如:
>>> import sys
>>> s = 'asdf'
>>> fcount(s)
2
>>> a = 1
>>> fcount(a)
605
  从对象1的引⽤计数信息也可以看到,python的对象缓冲池会缓存⼗分常⽤的immutable对象,⽐如这⾥的整数1。
  引⽤计数的优点在于原理通俗易懂;且将对象的回收分布在代码运⾏时:⼀旦对象不再被引⽤,就会被释放掉(be freed),不会造成卡顿。但也有缺点:额外的字段(ob_refcnt);频繁的加减ob_refcnt,⽽且可能造成连锁反应。但这些缺点跟循环引⽤⽐起来都不算事⼉。
  什么是循环引⽤,就是⼀个对象直接或者间接引⽤⾃⼰本⾝,引⽤链形成⼀个环。且看下⾯的例⼦:
1# -*- coding: utf-8 -*-
2import objgraph, sys
3class OBJ(object):
4pass
5
6def show_direct_cycle_reference():
7    a = OBJ()
8    a.attr = a
9    objgraph.show_backrefs(a, max_depth=5, filename = "direct.dot")
10
11def show_indirect_cycle_reference():
12    a, b = OBJ(), OBJ()
13    a.attr_b = b
14    b.attr_a = a
15    objgraph.show_backrefs(a, max_depth=5, filename = "indirect.dot")
16
17if__name__ == '__main__':
18if len(sys.argv) > 1:
19        show_direct_cycle_reference()
20else:
21        show_indirect_cycle_reference()
循环引⽤⽰例
  运⾏上⾯的代码,使⽤⼯具集(本⽂使⽤的是dotty)打开⽣成的两个⽂件,direct.dot 和 indirect.dot,得到下⾯两个图
  通过属性名(attr, attr_a, attr_b)可以很清晰的看出循环引⽤是怎么产⽣的
  前⾯已经提到,对于⼀个对象,当没有任何变量指向⾃⼰时,引⽤计数降到0,就会被释放掉。我们以上⾯左边那个图为例,可以看
到,红框⾥⾯的OBJ对象想在有两个引⽤(两个⼊度),分别来⾃帧对象frame(代码中,函数局部空间持有对OBJ实例的引⽤)、attr变量。我们再改⼀下代码,在函数运⾏技术之后看看是否还有OBJ类的实例存在,引⽤关系是怎么样的:
1# -*- coding: utf-8 -*-
2import objgraph, sys
3class OBJ(object):
4pass
5
6def direct_cycle_reference():
7    a = OBJ()
8    a.attr = a
9
10if__name__ == '__main__':
11    direct_cycle_reference()
12    objgraph.show_backrefs(objgraph.by_type('OBJ')[0], max_depth=5, filename = "direct.dot"
循环引⽤⽰例2
  修改后的代码,OBJ实例(a)存在于函数的local作⽤域。因此,当函数调⽤结束之后,来⾃帧对象frame的引⽤被解除。从图中可以看到,当前对象的计数器(⼊度)为1,按照引⽤计数的原理,是不应该被释放的,但这个对象在函数调⽤结束之后就是事实上的垃圾,这个时候就需要另外的机制来处理这种情况了。
  python的世界,很容易就会出现循环引⽤,⽐如标准库Collections中OrderedDict的实现(已去掉⽆关注释):
1class OrderedDict(dict):
2def__init__(self, *args, **kwds):
3if len(args) > 1:
4raise TypeError('expected at most 1 arguments, got %d' % len(args))
5try:
6            self.__root
7except AttributeError:
8            self.__root = root = []                    # sentinel node
9            root[:] = [root, root, None]
10            self.__map = {}
11        self.__update(*args, **kwds)
  注意第8、9⾏,root是⼀个列表,列表⾥⾯的元素之⾃⼰本⾝!
垃圾回收
  这⾥强调⼀下,本⽂中的的垃圾回收是狭义的垃圾回收,是指当出现循环引⽤,引⽤计数⽆计可施的时候采取的垃圾清理算法。
  在python中,使⽤标记-清除算法(mark-sweep)和分代(generational)算法来垃圾回收。在《》⼀⽂中有对标记回收算法,然后在《》⼀⽂中,有对前⽂的翻译,并且有分代回收的介绍。在这⾥,引⽤后⾯⼀篇⽂章:
  在Python中, 所有能够引⽤其他对象的对象都被称为容器(container). 因此只有容器之间才可能形成循环引⽤. Python的垃圾回收机制利⽤了这个特点来寻需要被释放的对象. 为了记录下所有的容器对象, Python将每⼀个容器都链到了⼀个双向链表中,之所以使⽤双向链表是为了⽅便快速的在容器集合中插⼊和删除对象. 有了这个维护了所有容器对象的双向链表以后, Python在垃圾回收时使⽤如下步骤来寻需要释放的对象:
1. 对于每⼀个容器对象, 设置⼀个gc_refs值, 并将其初始化为该对象的引⽤计数值.
2. 对于每⼀个容器对象, 到所有其引⽤的对象, 将被引⽤对象的gc_refs值减1.
3. 执⾏完步骤2以后所有gc_refs值还⼤于0的对象都被⾮容器对象引⽤着, ⾄少存在⼀个⾮循环引⽤. 因此不能释放这些对象, 将
他们放⼊另⼀个集合.
4. 在步骤3中不能被释放的对象, 如果他们引⽤着某个对象, 被引⽤的对象也是不能被释放的, 因此将这些
对象也放⼊另⼀个集
合中.
5. 此时还剩下的对象都是⽆法到达的对象. 现在可以释放这些对象了.
  关于分代回收:
  除此之外, Python还将所有对象根据’⽣存时间’分为3代, 从0到2. 所有新创建的对象都分配为第0代. 当这些对象经过⼀次垃圾回收仍然存在则会被放⼊第1代中. 如果第1代中的对象在⼀次垃圾回收之后仍然存货则被放⼊第2代. 对于不同代的对象Python的回收的频率也不⼀样. 可以通过gc.set_threshold(threshold0[, threshold1[, threshold2]])来定义. 当Python的垃圾回收器中新增的对象数量减去删除的对象数量⼤于threshold0时, Python会对第0代对象执⾏⼀次垃圾回收. 每当第0代被检查的次数超过了threshold1时, 第1代对象就会被执⾏⼀次垃圾回收. 同理每当第1代被检查的次数超过了threshold2时, 第2代对象也会被执⾏⼀次垃圾回收.
  注意,threshold0,threshold1,threshold2的意义并不相同!
  为什么要分代呢,这个算法的根源来⾃于weak generational hypothesis。这个假说由两个观点构成:⾸先是年亲的对象通常死得也快,⽐如⼤量的对象都存在于local作⽤域;⽽⽼对象则很有可能存活更长
的时间,⽐如全局对象,module, class。
  垃圾回收的原理就如上⾯提⽰,详细的可以看Python源码,只不过事实上垃圾回收器还要考虑__del__,弱引⽤等情况,会略微复杂⼀些。
  什么时候会触发垃圾回收呢,有三种情况:
  (1)达到了垃圾回收的阈值,Python虚拟机⾃动执⾏
  (2)⼿动调⽤gc.collect()
  (3)Python虚拟机退出的时候
  对于垃圾回收,有两个⾮常重要的术语,那就是reachable与collectable(当然还有与之对应的unreachable与uncollectable),后⽂也会⼤量提及。
  reachable是针对python对象⽽⾔,如果从根集(root)能到到对象,那么这个对象就是reachable,与之相反就是unreachable,事实上就是只存在于循环引⽤中的对象,Python的垃圾回收就是针对unreachable对象。
  ⽽collectable是针对unreachable对象⽽⾔,如果这种对象能被回收,那么是collectable;如果不能被回收,即循环引⽤中的对象定义了__del__,那么就是uncollectable。Python垃圾回收对uncollectable对象⽆能为⼒,会造成事实上的内存泄露。
gc module
  这⾥的gc(garbage collector)是Python 标准库,该module提供了与上⼀节“垃圾回收”内容相对应的接⼝。通过这个module,可以开关gc、调整垃圾回收的频率、输出调试信息。gc模块是很多其他模块(⽐如objgraph)封装的基础,在这⾥先介绍gc的核⼼API。
  gc.enable(); gc.disable(); gc.isenabled()
python是做什么的通俗易懂的  开启gc(默认情况下是开启的);关闭gc;判断gc是否开启
  gc.collection()
  执⾏⼀次垃圾回收,不管gc是否处于开启状态都能使⽤
  gc.set_threshold(t0, t1, t2); gc.get_threshold()
  设置垃圾回收阈值;获得当前的垃圾回收阈值
  注意:gc.set_threshold(0)也有禁⽤gc的效果
  gc.get_objects()
  返回所有被垃圾回收器(collector)管理的对象。这个函数⾮常基础!只要python解释器运⾏起来,就有⼤量的对象被collector管理,因此,该函数的调⽤⽐较耗时!
  ⽐如,命令⾏启动python
>>> import gc
>>> _objects())
3749
  gc.get_referents(*obj)
  返回obj对象直接指向的对象
  gc.get_referrers(*obj)
  返回所有直接指向obj的对象
  下⾯的实例展⽰了get_referents与get_referrers两个函数
>>> class OBJ(object):
... pass
...
>>> a, b = OBJ(), OBJ()

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。