Python2.7中函数内调用gc.get_objects为何引发循环引用/内存泄漏
问题背景
如下代码会产生循环引用,如果调用gc.disable()禁用自动垃圾回收,运行这段代码就会出现内存泄漏问题:
import gc, sys def test_func(): objs = gc.get_objects() print("-----End: test_func %s" % sys.getrefcount(test_func)) test_func()
已知该结论成立,但背后的原理不清晰,新增del objs的验证版本如下:
import gc, sys def test_func(): objs = gc.get_objects() del objs print("-----End: test_func %s" % sys.getrefcount(test_func)) test_func()
验证环境为Python 2.7。
核心原理
Python 2.7的内存管理以引用计数为核心,分代GC只做补充,专门处理引用计数搞不定的循环引用,这个问题的根因全在gc.get_objects()的返回特性和悄悄形成的引用环上:
- 首先明确
gc.get_objects()的行为:它会遍历返回当前解释器里所有被GC跟踪的对象,凑成一个列表返回,这个列表本身也是个被GC跟踪的容器对象。 - 当函数内部执行
objs = gc.get_objects()这一行时,会立刻生成两层引用计数无法处理的循环引用:- 栈帧与列表互指:调用
test_func时会生成对应的栈帧对象f,这个正在运行的栈帧本来就在GC跟踪范围内,自然会被塞进返回的objs列表,也就是列表l持有栈帧f的引用;反过来栈帧f的局部变量表中存储了objs变量,正好指向列表l,直接形成列表l -> 栈帧f -> 列表l的互指环。 - 列表自引用:Python 2.7中
gc.get_objects()构造返回列表的过程中,会把刚创建的列表对象本身也加入结果集,相当于l自己持有自己的引用,这也是一种典型的循环引用。
- 栈帧与列表互指:调用
- 引用计数的判定规则非常简单:只要对象的引用计数大于0就不会被回收。正常情况下函数执行结束时,调用栈会释放对栈帧
f的引用,栈帧销毁时会释放所有局部变量的引用,对应对象计数归0后自动回收。但因为上述循环引用的存在,回收流程直接卡壳:- 栈帧
f除了调用栈持有的1个引用外,还被列表l持有1个引用,调用栈释放自身持有的引用后,f的引用计数仍为1,不会被销毁; - 列表
l除了栈帧f持有的局部变量引用外,还被自己持有1个引用,就算栈帧释放了对l的引用,l的引用计数仍为1,不会被销毁。
- 栈帧
- 这类已经和外部代码完全断开、仅靠内部引用维持计数的对象环,只有在分代GC启动时,才会通过标记-清除算法识别并回收。如果手动调用
gc.disable()关闭自动GC,这部分对象占用的内存永远不会被释放,就会产生内存泄漏。 - 函数内部加
del objs没有解决问题的原因也很简单:del objs只会删除栈帧局部变量表中对列表l的引用,但列表l的自引用依然存在,l的引用计数始终为1,还是无法被引用计数机制回收,泄漏问题不会消失。
补充:Python 3.x调整了
gc.get_objects()的实现,返回的列表不会把自身纳入结果集,自引用问题不存在,但栈帧和列表的互指环依然存在,关闭GC运行照样会出现内存泄漏。
内容的提问来源于stack exchange,提问作者Grenter
相关产品推荐
相关产品推荐

