在 Python 中,为了解决内存泄漏问题,采用了对象引用计数,并基于引用计数实现自动垃圾回收。
因为 Python 有了自动垃圾回收功能,不少初学者就认为自己从此过上了好日子,不必再受内存泄漏的骚扰了。但如果查看一下 Python 文档对 __del__() 函数的描述,就知道好日子里也是有阴云的。下面摘抄一点文档内容:
Some common situations that may prevent the reference count of an object from going to zero include: circular references between objects (e.g., a doubly-linked list or a tree data structure with parent and child pointers); a reference to the object on the stack frame of a function that caught an exception (the traceback stored in sys.exc_traceback keeps the stack frame alive); or a reference to the object on the stack frame that raised an unhandled exception in interactive mode (the traceback stored in sys.last_traceback keeps the stack frame alive).
可见,有 __del__() 函数的对象间的循环引用是导致内存泄漏的主凶。特别说明:对没有 __del__() 函数的 Python 对象间的循环引用,是可以被自动垃圾回收掉的。
如何知道一个对象是否内存泄漏了呢?
方法一、当你认为一个对象应该被销毁时(即引用计数为 0),可以通过 sys.getrefcount(obj) 来获取对象的引用计数,并根据返回值是否为 0 来判断是否内存泄漏。如果返回的引用计数不为 0,说明在此刻对象 obj 是不能被垃圾回收器回收掉的。
方法二、也可以通过 Python 扩展模块 gc 来查看不能回收的对象的详细信息。
首先,来看一段正常的测试代码:
在 test_gcleak() 中,设置垃圾回收器调试标志后,再用 collect() 进行垃圾回收,最后打印出该次垃圾回收发现的不可达的垃圾对象数和整个解释器中的垃圾对象数。
gc.garbage 是一个 list 对象,列表项是垃圾收集器发现的不可达(即是垃圾对象)、但又不能释放(即不能回收)的对象。文档描述为:A list of objects which the collector found to be unreachable but could not be freed (uncollectable objects).
通常,gc.garbage 中的对象是引用环中的对象。因为 Python 不知道按照什么样的安全次序来调用环中对象的 __del__() 函数,导致对象始终存活在 gc.garbage 中,造成内存泄漏。如果知道一个安全的次序,那么就打破引用环,再执行 del gc.garbage[:] ,以清空垃圾对象列表。
上段代码输出为(#后字符串为笔者所加注释):
可见 _gcleak 对象的引用计数是正确的,也没有任何对象发生内存泄漏。
如果不注释掉 make_circle_ref() 中的 test_code_1 语句:
也就是让 _gcleak 形成一个自己对自己的循环引用。再运行上述代码,输出结果就变成:
可见 <CGcLeak 012AA090> 对象发生了内存泄漏!!而多出的 dict 垃圾就是泄漏的 _gcleak 对象的字典,打印出字典信息为:
除了对自己的循环引用,多个对象间的循环引用也会导致内存泄漏。简单举例如下:
这次测试后输出结果为:
可见 _a,_b 对象都发生了内存泄漏。因为二者是循环引用,垃圾回收器不知道该如何回收,也就是不知道该首先调用那个对象的 __del__() 函数。
采用以下任一方法,打破环状引用,就可以避免内存泄漏:
[1] 注释掉 make_circle_ref() 中的 test_code_2 语句;
[2] 注释掉 make_circle_ref() 中的 test_code_3 语句;
[3] 取消对 make_circle_ref() 中的 test_code_4 语句的注释。
相应输出结果变为:
结论:Python 的 gc 有比较强的功能,比如设置 gc.set_debug(gc.DEBUG_LEAK) 就可以进行循环引用导致的内存泄露的检查。如果在开发时进行内存泄露检查;在发布时能够确保不会内存泄露,那么就可以延长 Python 的垃圾回收时间间隔、甚至主动关闭垃圾回收机制,从而提高运行效率。
推荐阅读文献
1, Python垃圾回收算法描述: http://wiki.woodpecker.org.cn/moin/python_ref_circle_gc
2, Garbage Collection for Python: http://arctrix.com/nas/python/gc/
一、python有自动垃圾回收机制(当对象的引用计数为零时解释器会自动释放内存),出现内存泄露的场景一般是扩展库内存泄露或者循环引用(还有一种是全局容器里的对象没有删除)
前者无需讨论,后者举例如下(Obj('B')和Obj('C')的内存没有回收)(貌似循环引用的内存,Python解释器也会自己回收(标记-清除垃圾收集机制),也就是说我们在编码中不需要耗费精力去刻意避免循环引用,具体的内容这两天再细看一下---2013.10.20)
- [dongsong@localhost python_study]$ cat leak_test2.py
- #encoding=utf-8
- class Obj:
- def __init__(self,name='A'):
- self.name = name
- print '%s inited' % self.name
- def __del__(self):
- print '%s deleted' % self.name
- if __name__ == '__main__':
- a = Obj('A')
- b = Obj('B')
- c = Obj('c')
- c.attrObj = b
- b.attrObj = c
- [dongsong@localhost python_study]$ vpython leak_test2.py
- A inited
- B inited
- c inited
- A deleted
该模块可以找到增长最快的对象、实际最多的对象,可以画出某对象里面所有元素的引用关系图、某对象背后的所有引用关系图;可以根据地址获取对象
但是用它来找内存泄露还是有点大海捞针的感觉:需要自己更具增长最快、实际最多对象的日志来确定可疑对象(一般是list/dict/tuple等common对象,这个很难排查;如果最多最快的是自定义的非常规对象则比较好确定原因)
1.show_refs() show_backrefs() show_most_common_types() show_growth()
- [dongsong@localhost python_study]$ !cat
- cat objgraph1.py
- #encoding=utf-8
- import objgraph
- if __name__ == '__main__':
- x = []
- y = [x, [x], dict(x=x)]
- objgraph.show_refs([y], filename='/tmp/sample-graph.png') #把[y]里面所有对象的引用画出来
- objgraph.show_backrefs([x], filename='/tmp/sample-backref-graph.png') #把对x对象的引用全部画出来
- #objgraph.show_most_common_types() #所有常用类型对象的统计,数据量太大,意义不大
- objgraph.show_growth(limit=4) #打印从程序开始或者上次show_growth到现在增加的对象(按照增加量的大小排序)
- [dongsong@localhost python_study]$ !vpython
- vpython objgraph1.py
- Graph written to /tmp/tmpuSFr9A.dot (5 nodes)
- Image generated as /tmp/sample-graph.png
- Graph written to /tmp/tmpAn6niV.dot (7 nodes)
- Image generated as /tmp/sample-backref-graph.png
- tuple 3393 +3393
- wrapper_descriptor 945 +945
- function 830 +830
- builtin_function_or_method 622 +622
sample-graph.png
sample-backref-graph.png
2.show_chain()
- [dongsong@localhost python_study]$ cat objgraph2.py
- #encoding=utf-8
- import objgraph, inspect, random
- class MyBigFatObject(object):
- pass
- def computate_something(_cache = {}):
- _cache[42] = dict(foo=MyBigFatObject(),bar=MyBigFatObject())
- x = MyBigFatObject()
- if __name__ == '__main__':
- objgraph.show_growth(limit=3)
- computate_something()
- objgraph.show_growth(limit=3)
- objgraph.show_chain(
- objgraph.find_backref_chain(random.choice(objgraph.by_type('MyBigFatObject')),
- inspect.ismodule),
- filename = '/tmp/chain.png')
- #roots = objgraph.get_leaking_objects()
- #print 'len(roots)=%d' % len(roots)
- #objgraph.show_most_common_types(objects = roots)
- #objgraph.show_refs(roots[:3], refcounts=True, filename='/tmp/roots.png')
- [dongsong@localhost python_study]$ !vpython
- vpython objgraph2.py
- tuple 3400 +3400
- wrapper_descriptor 945 +945
- function 831 +831
- wrapper_descriptor 956 +11
- tuple 3406 +6
- member_descriptor 165 +4
- Graph written to /tmp/tmpklkHqC.dot (7 nodes)
- Image generated as /tmp/chain.png
chain.png
三、gc模块
该模块可以确定垃圾回收期无法引用到(unreachable)和无法释放(uncollectable)的对象,跟objgraph相比有其独到之处
gc.collect()强制回收垃圾,返回unreachable object的数量
gc.garbage返回unreachable object中uncollectable object的列表(都是些有__del__()析构函数并且身陷引用循环的对象)IfDEBUG_SAVEALL is set, then all unreachable objects will be added to this list rather than freed.
warning:如果用gc.disable()把自动垃圾回收关掉了,然后又不主动gc.collect(),你会看到内存刷刷的被消耗....
- [dongsong@bogon python_study]$ cat gc_test.py
- #encoding=utf-8
- import gc
- class MyObj:
- def __init__(self, name):
- self.name = name
- print "%s inited" % self.name
- def __del__(self):
- print "%s deleted" % self.name
- if __name__ == '__main__':
- gc.disable()
- gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE | gc.DEBUG_INSTANCES | gc.DEBUG_OBJECTS | gc.DEBUG_SAVEALL)
- a = MyObj('a')
- b = MyObj('b')
- c = MyObj('c')
- a.attr = b
- b.attr = a
- a = None
- b = None
- c = None
- if gc.isenabled():
- print 'automatic collection is enabled'
- else:
- print 'automatic collection is disabled'
- rt = gc.collect()
- print "%d unreachable" % rt
- garbages = gc.garbage
- print "\n%d garbages:" % len(garbages)
- for garbage in garbages:
- if isinstance(garbage, MyObj):
- print "obj-->%s name-->%s attrrMyObj-->%s" % (garbage, garbage.name, garbage.attr)
- else:
- print str(garbage)
- [dongsong@bogon python_study]$ vpython gc_test.py
- a inited
- b inited
- c inited
- c deleted
- automatic collection is disabled
- gc: uncollectable <MyObj instance at 0x7f3ebd455b48>
- gc: uncollectable <MyObj instance at 0x7f3ebd455b90>
- gc: uncollectable <dict 0x261c4b0>
- gc: uncollectable <dict 0x261bdf0>
- 4 unreachable
- 4 garbages:
- obj--><__main__.MyObj instance at 0x7f3ebd455b48> name-->a attrrMyObj--><__main__.MyObj instance at 0x7f3ebd455b90>
- obj--><__main__.MyObj instance at 0x7f3ebd455b90> name-->b attrrMyObj--><__main__.MyObj instance at 0x7f3ebd455b48>
- {'name': 'a', 'attr': <__main__.MyObj instance at 0x7f3ebd455b90>}
- {'name': 'b', 'attr': <__main__.MyObj instance at 0x7f3ebd455b48>}