2011-06-09 46 views
6

我發現exec(它發生在一個系統中,必須用用戶編寫的腳本來擴展)的問題。我可以在問題本身降低到這個代碼:Python: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 
+0

在你的代碼中做同樣的事情(沒有通過字符串和'exec')給出相同的結果嗎? – delnan 2011-06-09 18:02:07

+3

'gc.collect()'似乎解決了它。某處必須有一個循環循環。瘋狂地猜測,x具有對其類的引用,該類可能具有對其定義的名稱空間的引用,並且該引用又具有對x的引用。 – 2011-06-09 18:04:41

+0

在沒有exec的代碼中,同樣的事情進行得很順利,並且垃圾收集器按預期工作。 – 3xter 2011-06-09 18:07:33

回答

5

,你看到它佔用200MB的內存比預期的更長的原因是因爲你有一個參考週期:context是字典參考xtestx參考test的實例,其參考文獻testtest有一個屬性字典,test.__dict__,其中包含該類的__init__函數。 __init__函數反過來引用它定義的全局變量 - 這是您傳遞給exec,context的字典。

Python會爲你分解這些參考週期(因爲沒有涉及的方法有__del__方法),但它需要運行gc.collect()gc.collect()會每N次分配(由gc.set_threshold()確定)自動運行,所以「泄漏」在某個時刻會消失,但如果您希望它立即消失,您可以自己運行gc.collect(),或者在退出該功能之前自行中斷參考週期。您可以通過致電context.clear()輕鬆完成後者 - 但您應該認識到這會影響您在其中創建的類的所有實例。

0

我不認爲這個問題與exec有關 - 垃圾收集器只是沒有激活。如果你出去提取exec「d代碼到主應用程序,這兩種方式給予相同的行爲與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,並且可能會提供有關如何阻止該事件發生的線索。

我知道我沒有真正解決這個問題,但希望這可以幫助您朝着正確的方向發展。我會記住這個問題,以防萬一我稍後再發現。

+0

循環垃圾收集器不會「啓動」來銷燬局部變量 - 它本身並不參與局部變量。 Python使用引用計數,並且銷燬局部變量就像一個遞減操作一樣簡單。 'gc'模塊的收集器是一個單獨的東西,並且當分配閾值被觸發時(或者當你手動調用它時)才真正觸發。) – 2011-06-09 23:32:21

+0

@Thomas:哦,我明白了。然而,手動調用'gc.collect()'不會破壞'x',如果它不在'exec'中計算。爲什麼會這樣? – voithos 2011-06-09 23:39:07

+0

我不確定你在這裏描述的是哪種情況。 cyclic-gc收集器將僅收集*無法訪問的*,可收集的參考週期。對象的引用。 'x'是一個局部變量 - 它是一個名字,而不是一個對象。它有一個參考。 'x' *這個東西指的是*可能無法訪問,但只有'x'不再存在。或者,'x'指的是可以成爲不可達參考循環的一部分,但是隻有包含'x'的框架也是該不可達參考循環的一部分。 – 2011-06-09 23:42:43