Python:exec语句和意外的垃圾收集器行为

时间:2011-06-09 17:44:51

标签: python garbage-collection exec

我发现了exec的问题(它发生在一个必须可以用用户编写的脚本扩展的系统中)。我可以将问题本身减少到这段代码:

def fn():
    context = {}
    exec '''
class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
x = test()''' in context

fn()

我预计在调用函数fn之后,垃圾收集器应该释放内存。但是,Python进程仍然消耗额外的200MB内存,我完全不知道这里发生了什么以及如何手动释放分配的内存。

我怀疑在exec中定义一个类并不是一个非常明智的想法,但首先,我想了解上面示例中出现的问题。

看起来在另一个函数中创建包装类实例可以解决问题,但有什么区别?

def fn():
    context = {}
    exec '''
class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()
    ''' in context
fn()

这是我的Python解释器版本:

$ python
Python 2.7 (r27:82500, Sep 16 2010, 18:02:00) 
[GCC 4.5.1 20100907 (Red Hat 4.5.1-3)] on linux2

2 个答案:

答案 0 :(得分:5)

您看到它占用200Mb内存超过预期的原因是因为您有一个参考周期:context是一个引用xtest的字典。 x引用test的实例,该实例引用testtest有一个属性词典test.__dict__,其中包含该类的__init__函数。 __init__函数依次引用它所定义的全局变量 - 这是你传递给execcontext的字典。

Python将为您打破这些参考周期(因为没有涉及__del__方法),但它需要gc.collect()才能运行。 gc.collect()将自动运行每N次分配(由gc.set_threshold()确定),因此“泄漏”会在某个时刻消失,但如果您希望它立即消失,您可以自己运行gc.collect() ,或者在退出函数之前自己打破参考周期。您可以通过调用context.clear()轻松完成后者 - 但您应该意识到这会影响您在其中创建的类的所有实例。

答案 1 :(得分:0)

我不认为问题与exec有关 - 垃圾收集器没有激活。如果您将exec代码解压缩到主应用程序中,则两种方式都会产生与exec相同的行为:

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
x = test()

# Consumes 200MB

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()

# Memory get collected correctly

两种方法之间的区别在于,在第二种方法中,本地范围在调用f1()时会发生变化,我认为当x超出范围时垃圾收集器会启动该函数将控制权返回给主脚本。如果范围没有改变,那么垃圾收集器会等待until the difference between the number of allocations and the number of deallocations exceeds its threshold(在我的机器上,默认情况下阈值为700 - 运行Python 2.7)。

我们可以弄清楚发生了什么:

import sys
import gc

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
x = test()

print gc.get_count()
# Prints (168, 8, 0)

因此,我们看到垃圾收集器多次启动,但由于某种原因不收集x。如果您使用其他版本进行测试:

import sys
import gc

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()

print gc.get_count()
# Prints (172, 8, 0)

在这种情况下,我们知道它确实设法收集x。因此,似乎在全局范围内声明x时,它会保留一些自身的循环引用,以防止它被收集。我们总是可以使用del x手动强制收集,但当然这并不理想。如果使用gc.get_referrers(x),我们将能够看到哪些对象仍然引用x,也许这将提供如何阻止这种情况发生的线索。

我知道我并没有真正解决这个问题,但希望这能帮助你朝着正确的方向前进。我会记住这个问题,以防我稍后发现。