垃圾收集

從本質上講,Python 的垃圾收集器(從 3.5 開始)是一個簡單的引用計數實現。每次引用物件(例如,a = myobject)時,該物件(myobject)上的引用計數都會遞增。每次刪除引用時,引用計數都會遞減,一旦引用計數達到 0,我們就知道沒有任何內容可以引用該物件,我們可以釋放它!

關於 Python 記憶體管理如何工作的一個常見誤解是 del 關鍵字釋放了物件記憶體。這不是真的。實際發生的事情是 del 關鍵字只是遞減物件 refcount,這意味著如果你呼叫它足夠多次以使 refcount 達到零,那麼物件可能被垃圾收集(即使實際上仍然存在對程式碼中其他地方可用的物件的引用) )。

Python 在第一次需要它時就會建立或清理物件如果我執行賦值 a = object(),那時就會分配物件的記憶體(cpython 有時會重用某些型別的物件,例如引擎蓋下的列表,但主要是它不保留一個免費的物件池,並在你需要時執行分配)。同樣,只要 refcount 減少到 0,GC 就會清除它。

世代垃圾收集

在 1960 年代,John McCarthy 在實現 Lisp 使用的引用計算演算法時發現了引用垃圾收集的致命缺陷:如果兩個物件在迴圈引用中相互引用會發生什麼?你怎麼能垃圾收集這兩個物件,即使它們沒有外部引用,如果它們總是指向彼此?此問題還擴充套件到任何迴圈資料結構,例如環形緩衝區或雙向連結串列中的任何兩個連續條目。Python 嘗試使用另一種稱為 Generational Garbage Collection 的垃圾收集演算法稍微有趣的轉折來解決這個問題。

實質上,每當你在 Python 中建立物件時,它都會將其新增到雙向連結列表的末尾。有時 Python 會迴圈遍歷此列表,檢查列表中物件所引用的物件,如果它們也在列表中(我們將看到它們可能不會出現的原因),則進一步減少它們的引用。此時(實際上,有一些啟發式方法可以確定事情何時被移動,但讓我們假設它是在單個集合之後保持簡單)任何仍然具有大於 0 的引用計數的東西都會被提升到另一個名為“第 1 代”的連結串列(這就是為什麼所有物件並不總是在第 0 代列表中),這種迴圈應用的次數較少。這是世代垃圾收集的用武之地。預設情況下,Python 中有 3 代(三個連結的物件列表): 第一個列表(第 0 代)包含所有新物件; 如果 GC 迴圈發生並且沒有收集物件,它們將被移動到第二個列表(第 1 代),如果 GC 迴圈發生在第二個列表上並且仍未收集它們,則它們將被移動到第三個列表(第 2 代) )。第三代列表(稱為“第 2 代”,因為我們是零索引)比前兩個更少地進行垃圾收集,這個想法是如果你的物件是長壽的,那麼它不太可能被 GCed,並且可能永遠不會在應用程式的生命週期內進行 GC 操作,因此在每次執行 GC 時都沒有浪費時間檢查它。此外,觀察到大多數物件相對較快地被垃圾收集。從現在開始,我們將這些好東西稱為年輕人。這被稱為“

快速擱置:與前兩代不同,長期存在的第三代清單不是定期垃圾收集。檢查長壽命待決物件(第三代列表中的那些,但實際上還沒有 GC 迴圈)與列表中的總長壽命物件的比率大於 25%。這是因為第三個列表是無限的(事物永遠不會從它移到另一個列表中,因此它們只在實際上被垃圾收集時才會消失),這意味著對於建立大量長壽命物件的應用程式,GC 迴圈在第三個列表上可以得到很長時間。通過使用比率,我們實現在物件總數中的攤銷線性表現; 也就是說,列表越長,GC 越長,但我們執行 GC 的次數越少(這裡是 由 MartinvonLöwis 撰寫的關於此啟發式的 2008 年原始提案 ,以供進一步閱讀)。在第三代或成熟列表上執行垃圾收集的行為稱為完全垃圾收集

因此,通過不要求我們一直掃描不太可能需要 GC 的物件,分代垃圾收集可以加快速度,但是它如何幫助我們打破迴圈引用呢?事實證明,可能不太好。實際打破這些參考週期的功能開始如下

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

分代垃圾收集的原因在於我們可以將列表的長度保持為單獨的計數; 每次我們向生成新增一個新物件時,我們都會增加此計數,並且每當我們將一個物件移動到另一代或解除它時,我們就會減少計數。理論上在 GC 迴圈結束時,這個計數(前兩代反正)應該總是為 0.如果不是,那麼剩下的列表中的任何東西都是某種形式的迴圈引用,我們可以放棄它。但是,還有一個問題:如果剩下的物件上有 Python 的魔法方法 __del__ 怎麼辦?每當 Python 物件被銷燬時都會呼叫 __del__。但是,如果迴圈引用中的兩個物件具有 __del__ 方法,我們無法確定銷燬其中一個不會破壞其他物件 __del__ 方法。

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

我們設定一個 A 例項和一個 B 例項指向另一個,然後它們最終進入同一個垃圾收集週期?假設我們隨機選擇一個並且首先取消我們的 A 例項; 將呼叫 A 的 __del__ 方法,它將列印,然後 A 將被釋放。接下來我們來到 B,我們稱之為 __del__ 方法,然後哎呀! 段錯誤! A 不再存在。我們可以通過首先呼叫剩下的 __del__ 方法來解決這個問題,然後做另一個傳遞來實際解除所有內容,但是,這引入了另一個問題:如果一個物件 __del__ 方法儲存了另一個即將被 GCed 的物件的引用怎麼辦?在其他地方有我們的參考?我們仍然有一個參考週期,但現在實際上 GC 不可能是物件,即使它們已經不再使用了。請注意,即使一個物件不是迴圈資料結構的一部分,它也可以用自己的 __del__ 方法恢復自身; Python 確實對此進行了檢查,如果在呼叫 __del__ 方法後物件引用計數增加,則會停止 GCing。

CPython 處理這個問題的方法是將那些不具有 GC 功能的物件(任何帶有某種形式的迴圈引用和 __del__ 方法的物件)貼上到一個無法收集的垃圾的全域性列表中,然後將它留在那裡永恆:

/* list of uncollectable objects */
static PyObject *garbage = NULL;