1. 程式人生 > 實用技巧 >轉載-python垃圾回收機制(Garbage collection)

轉載-python垃圾回收機制(Garbage collection)

原文連結:https://www.cnblogs.com/xiugeng/p/10514101.html#_label0_0

python垃圾回收機制(Garbage collection)

目錄

正文

  由於面試中遇到了垃圾回收的問題,轉載學習和總結這個問題。

  在C/C++中採用使用者自己管理維護記憶體的方式。自己管理記憶體極其自由,可以任意申請記憶體,但也為大量記憶體洩露、懸空指標等bug埋下隱患。

  因此在現在的高階語言(java、C#等)都採用了垃圾收集機制。

  python也採用了垃圾收集機制,採用引用計算機制為主,標記——清除和分代收集兩種機制為輔的策略。

回到頂部

一、引用計數機制

  python裡每一個東西都是物件,它們的核心就是一個結構體:PyObject。

typedef struct_object {
     int ob_refcnt;
     struct_typeobject *ob_type;
} PyObject;

  PyObject是每個物件必有的內容,其中ob_refcnt就是做為引用計數。當一個物件有新的引用時,它的ob_refcnt就會增加,當引用它的物件被刪除,它的ob_refcnt就會減少。

#define Py_INCREF(op)   ((op)->ob_refcnt++)          //增加計數
#define Py_DECREF(op)      \                         //減少計數        
     if (--(op)->ob_refcnt != 0)    \
         ;        \
     else         \
         __Py_Dealloc((PyObject *)(op))

  引用計數為0時,該物件宣告就結束了。

1、引用計數機制優點

  • 簡單
  • 實時性:一旦沒有引用,記憶體就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收記憶體的時間分攤到了平時

2、引用計數機制缺點

  • 維護引用計數消耗資源
  • 迴圈引用
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)

  list1與list2相互引用,如果不存在其他物件對它們的引用,list1與list2的引用計數也仍然為1,所佔用的記憶體永遠無法被回收,這將是致命的。

  對於如今的強大硬體,缺點1尚可接受,但是迴圈引用導致記憶體洩露,註定python還將引入新的回收機制(標記清除和分代收集)。

  轉載地址:http://my.oschina.net/hebianxizao/blog/57367?fromerr=KJozamtm

回到頂部

二、Garbage collection(GC)

  英文原文:visualizing garbage collection in ruby and python

  中文:畫說 Ruby 與 Python 垃圾回收

  GC系統所承擔的工作遠比“垃圾回收”多得多。它們負責三個重要任務:

  • 為新生成的物件分配記憶體
  • 識別那些垃圾物件
  • 從垃圾物件回收記憶體

  如果將應用程式比作人的身體:所有你所寫的那些優雅的程式碼,業務邏輯,演算法,應該就是大腦。垃圾回收就是應用程式那顆躍動的心。像心臟為身體其他器官提供血液和營養物那樣,垃圾回收器為你的應該程式提供記憶體和物件。如果垃圾回收器停止工作或執行遲緩,像動脈阻塞,你的應用程式效率也會下降,直至最終死掉。

1、簡單程式碼示例

  運用例項來幫助理論理解,下面是一個簡單類,分別用python和ruby寫成:

  

2、Ruby——可用列表

  在ruby中執行上面的Node.new(1)時,Ruby到底做了什麼?Ruby是如何為我們建立新的物件的呢?

  出乎意料的是它做的非常少。實際上,早在程式碼開始執行前,Ruby就提前建立了成百上千個物件,並把它們串在連結串列上,名曰:可用列表。下圖所示為可用列表的概念圖:

  

  想象一下每個白色方格都標著一個“未使用預建立物件”。當呼叫Node.new時,Ruby只需要取一個預建立物件給我們使用即可:

  

  上圖中左側灰格表示我們程式碼中使用的當前物件,同時其他白格是未使用物件。(請注意:無疑我的示意圖是對實際的簡化。實際上,Ruby會用另一個物件來裝載字串"ABC",另一個物件裝載Node類定義,還有一個物件裝載了程式碼中分析出的抽象語法樹,等等)

  如果我們再次呼叫Node.new,Ruby將遞給我們另一個物件:   

  這個簡單的用連結串列來預分配物件的演算法已經發明瞭超過50年,發明者是電腦科學家John McCarthy,一開始是用Lisp語言實現。

  Lisp不僅是最早的函數語言程式設計語言,在電腦科學領域也有許多創舉。其一就是利用垃圾回收機制自動化進行程式記憶體管理的概念。

  標準版的Ruby,也就是眾所周知的"Matz's Ruby Interpreter"(MRI),所使用的GC演算法與McCarthy在1960年的實現方式很類似。

  Ruby的垃圾回收機制像Lisp一樣,預先建立一些物件,然後在你分配新物件或者變數的時候供你使用。

3、Python——物件分配

  由於許多原因Python也使用可用列表(用來回收一些特定物件比如 list),但在為新物件和變數分配記憶體的方面Python和Ruby是不同的。

  例如我們用Pyhon來建立一個Node物件:

  

  當建立物件時Python立即向作業系統請求記憶體。(Python實際上實現了一套自己的記憶體分配系統,在作業系統堆之上提供了一個抽象層。)

  當建立第二個物件的時候,再次向OS請求記憶體:

  

  可以看到,在建立物件的時候,Python會花些時間為我們找到並分配記憶體。

4、Ruby和Python垃圾回收對比

(1)Ruby開發者像住在凌亂的房間裡

  

  Ruby把無用的物件留在記憶體裡,直到下一次GC執行。隨著我們建立越來越多的物件,Ruby會持續尋可用列表裡取預建立物件給我們。因此,可用列表會逐漸變短:

  

  甚至變得更短:

  

  一直在為變數n1賦新值,Ruby把舊值留在原處。"ABC","JKL"和"MNO"三個Node例項還滯留在記憶體中。Ruby不會立即清除程式碼中不再使用的舊物件!

  Ruby開發者們就像是住在一間凌亂的房間,地板上摞著衣服,要麼洗碗池裡都是髒盤子。作為一個Ruby程式設計師,無用的垃圾物件會一直環繞著你。

(2)Python開發者像住在衛生之家

  

  用完的垃圾物件會立即被python打掃乾淨。

  Python與Ruby的垃圾回收機制頗為不同。讓我們回到前面提到的三個Python Node物件:

  

  在內部,建立一個物件時,Python總是在物件的C結構體裡儲存一個整數,稱為引用數。期初,Python將這個值設定為1:

  

  值為1說明分別有個一個指標指向或是引用這三個物件。假如我們現在建立一個新的Node例項,JKL:

  

  與之前一樣,Python設定JKL的引用數為1。然而,請注意由於我們改變了n1指向了JKL,不再指向ABC,Python就把ABC的引用數置為0了。
此刻,Python垃圾回收器立刻挺身而出!每當物件的引用數減為0,Python立即將其釋放,把記憶體還給作業系統

  

  上面Python回收了ABC Node例項使用的記憶體。而Ruby棄舊物件原地於不顧,也不釋放它們的記憶體。

  Python的這種垃圾回收演算法被稱為引用計數。是George-Collins在1960年發明的,恰巧與John McCarthy發明的可用列表演算法在同一年出現。就像Mike-Bernstein在6月份哥譚市Ruby大會傑出的垃圾回收機制演講中說的: "1960年是垃圾收集器的黃金年代..."

  再讓n2引用n1:

  

  上圖中左邊的DEF的引用數已經被Python減少了,垃圾回收器會立即回收DEF例項。同時JKL的引用數已經變為了2 ,因為n1和n2都指向它。

回到頂部

三、標記——清除演算法

  Ruby程式執行一陣子後,可用列表最終被用盡了,最終凌亂房間充斥著垃圾。   

  此刻所有Ruby預建立物件都被程式用過了(都變灰了),可用列表中空空如也(沒有白格子)。

  Ruby祭出另一McCarthy發明的演算法,名曰:標記-清除。首先Ruby把程式停下來,Ruby用"地球停轉垃圾回收大法"。之後Ruby輪詢所有指標,變數和程式碼產生別的引用物件和其他值。同時Ruby通過自身的虛擬機器便利內部指標。標記出這些指標引用的每個物件,在圖中使用M表示。

  

  上圖中那三個被標M的物件是程式還在使用的。在內部,Ruby實際上使用一串位值,被稱為:可用點陣圖(譯註:還記得《程式設計珠璣》裡的為突發排序嗎,這對離散度不高的有限整數集合具有很強的壓縮效果,用以節約機器的資源。),來跟蹤物件是否被標記了。

  

  Ruby將這個可用點陣圖存放在獨立的記憶體區域中,以便充分利用Unix的寫時拷貝化。有關此事的更多內容請關注我另一博文《Why You Should Be Excited About Garbage Collection in Ruby 2.0》

  如果說被標記的物件是存活的,剩下的未被標記的物件只能是垃圾。下圖中用白格子來標識垃圾物件:

  

  Ruby來清除這些無用的垃圾物件,把它們送回到可用列表中:

  

  Ruby實際上不會吧物件從這拷貝到那。而是通過調整內部指標,將其指向一個新連結串列的方式,來將垃圾物件歸位到可用列表中的。

現在等到下回再建立物件的時候Ruby又可以把這些垃圾物件分給我們使用了。在Ruby裡,物件們六道輪迴,轉世投胎,享受多次人生。

回到頂部

四、標記—刪除 vs 引用計數

  咋一看,python的GC演算法似乎遠勝於Ruby:寧舍潔宇而居穢室乎?為什麼Ruby寧願定期強制程式停止執行,也不使用Python的演算法呢?

  引用計數並不像第一眼看上去那樣簡單。有許多原因使得不許多語言不像Python這樣使用引用計數GC演算法:

  1、引用計數GC演算法不好實現。

  Python不得不在每個物件內部留一些空間來處理引用數。這樣付出了一小點兒空間上的代價。但更糟糕的是,每個簡單的操作(像修改變數或引用)都會變成一個更復雜的操作,因為Python需要增加一個計數,減少另一個,還可能釋放物件。

  2、引用計數GC演算法相對較慢。

  雖然Python隨著程式執行GC很穩健(一把髒碟子放在洗碗盆裡就開始洗啦),但這並不一定更快。Python不停地更新著眾多引用數值。特別是當你不再使用一個大資料結構的時候,比如一個包含很多元素的列表,Python可能必須一次性釋放大量物件。減少引用數就成了一項複雜的遞迴過程了。

  3、引用計數GC演算法並不總是奏效。

  引用計數不能處理環形資料結構--也就是含有迴圈引用的資料結構。

回到頂部

五、Python的代式垃圾回收

  英文原文地址:Generational GC in Python and Ruby
  中文原文:對比Ruby和Python的垃圾回收(2):代式垃圾回收機制

1、python 引用計數演算法

  標準Ruby(也被稱為Matz的Ruby直譯器或是MRI)使用名為標記回收(Mark and Sweep)的垃圾回收演算法,這個演算法是為1960年原版本的Lisp所開發。

  Python使用一種有53年曆史的GC演算法,這種演算法的思路非常不同,稱之為引用計數

  Python在引用計數之外,還用了另一個名為Generational Garbage Collection的演算法。這意味著Python的垃圾回收器用不同的方式對待新建立的以及舊有的物件。

  在Python中,每個物件都儲存了一個稱為引用計數的整數值,來追蹤到底有多少引用指向了這個物件。

  無論何時,如果我們程式中的一個變數或其他物件引用了目標物件,Python將會增加這個計數值,而當程式停止使用這個物件,則Python會減少這個計數值。一旦計數值被減到零,Python將會釋放這個物件以及回收相關記憶體空間。

2、引用計數存在的問題(迴圈資料結構)

  從二十世紀六十年代起,電腦科學界就面臨一個嚴重的理論問題:針對引用計數演算法來說,如果一個數據結構引用了它自身,則這個資料結構是一個迴圈資料結構,那麼某些引用計數值是肯定無法變成零的。如下例所示:

  

  上面程式碼中的構造器(Python中叫init),在一個例項變數中儲存一個單獨的屬性。在類定義之後建立兩個節點,ABC和DEF,在圖中為左邊的矩形框。

  兩個節點的引用計數都被初始化為1,因為各有兩個引用指向各個節點(n1和n2)。再在節點中定義兩個附加的屬性:next和prev。

  

  Python中可以在程式碼執行的時候動態定義例項變數或物件屬性。設定 n1.next 指向 n2,同時設定 n2.prev 指回 n1。現在,我們的兩個節點使用迴圈引用的方式構成了一個雙端連結串列。同時請注意到 ABC 以及 DEF 的引用計數值已經增加到了2。這裡有兩個指標指向了每個節點:首先是 n1 以及 n2,其次就是 next 以及 prev。

  然後假定程式不再使用這兩個節點,將n1和n2都設定為None。

  

  此時,python如同往常一樣將每個節點的引用計數減少到1。

  一個“孤島”或是一組未使用的、互相指向的物件,但是誰都沒有外部引用。換句話說,我們的程式不再使用這些節點物件了,所以我們希望Python的垃圾回收機制能夠足夠智慧去釋放這些物件並回收它們佔用的記憶體空間。但是這不可能,因為所有的引用計數都是1而不是0Python的引用計數演算法不能夠處理互相指向自己的物件

3、python中的零代(Generation Zero)

  程式碼也許會在不經意間包含迴圈引用而且程式設計師往往未意識到。

  事實上,Python程式執行的時候它將會建立一定數量的“浮點數垃圾”,Python的GC不能夠處理未使用的物件因為應用計數值不會到零。

  因此python要引入Generation GC演算法。如Ruby使用一個連結串列(free list)來持續追蹤未使用的、自由的物件一樣,Python使用一種不同的連結串列來持續追蹤活躍的物件。而不將其稱之為“活躍列表”,Python的內部C程式碼將其稱為零代(Generation Zero)。每次當你建立一個物件或其他什麼值的時候,Python會將其加入零代連結串列。

  

  從上邊可以看到當我們建立ABC節點的時候,Python將其加入零代連結串列。請注意到這並不是一個真正的列表,並不能直接在你的程式碼中訪問,事實上這個連結串列是一個完全內部的Python執行時。

  相似的,當我們建立DEF節點的時候,Python將其加入同樣的連結串列:

  

  此時,零代包含了兩個節點物件。(他還將包含Python建立的每個其他值,與一些Python自己使用的內部值。)

4、檢測迴圈引用

  隨後,Python會迴圈遍歷零代列表上的每個物件,檢查列表中每個互相引用的物件,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放物件。

(1)迴圈檢測示例

    

  從上面可以看到 ABC 和 DEF 節點包含的引用數為1。有三個其他的物件同時存在於零代連結串列中,藍色的箭頭指示了有一些物件正在被零代連結串列之外的其他物件所引用。(接下來我們會看到,Python中同時存在另外兩個分別被稱為一代和二代的連結串列)。這些物件有著更高的引用計數因為它們正在被其他指標所指向著。

(2)python的GC處理零代連結串列

  

  通過識別內部引用,Python能夠減少許多零代連結串列物件的引用計數。在上圖的第一行中你能夠看見ABC和DEF的引用計數已經變為零了,這意味著收集器可以釋放它們並回收記憶體空間了。剩下的活躍的物件則被移動到一個新的連結串列:一代連結串列

  從某種意義上說,Python的GC演算法類似於Ruby所用的標記回收演算法。週期性地從一個物件到另一個物件追蹤引用以確定物件是否還是活躍的,正在被程式所使用的,這正類似於Ruby的標記過程。

(3)python中的GC閾值

  python中什麼時候進行這個標記過程?

  隨著程式執行,Python直譯器保持對新建立的物件和因引用計數為零而被釋放掉的物件的追蹤。從理論上說,這兩個值應該保持一致,因為程式新建的每個物件都應該最終被釋放掉。

  但在實際情況中,由於迴圈或程式使用了一些比其他物件存在時間更長的物件,被分配物件的計數值與被釋放物件的計數值之間的差異在逐漸增長。一旦差異累計超過某個閾值,則Python的收集機制就啟動了,並且觸發上邊所說到的零代演算法,釋放“浮動的垃圾”,並且將剩下的物件移動到一代列表。

  隨著時間的推移,程式所使用的物件逐漸從零代列表移動到一代列表。而Python對於一代列表中物件的處理遵循同樣的方法,一旦被分配計數值與被釋放計數值累計到達一定閾值,Python會將剩下的活躍物件移動到二代列表。

  通過這種方法,你的程式碼所長期使用的物件,那些你的程式碼持續訪問的活躍物件,會從零代連結串列轉移到一代再轉移到二代。通過不同的閾值設定,Python可以在不同的時間間隔處理這些物件。Python處理零代最為頻繁,其次是一代然後才是二代

5、弱代假說(weak generational hypothesis)

  代垃圾回收演算法的核心行為:垃圾回收器會更頻繁的處理新物件。一個新的物件即是你的程式剛剛建立的,而一個來的物件則是經過了幾個時間週期之後仍然存在的物件。

  python會在當一個物件從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個物件。這麼做的緣故是弱代假說

  弱代假說由兩個觀點構成:年輕的物件通常死得也快,而老物件則很可能存活更長時間。

  假定現在用Python或Ruby建立一個新物件:

  

  根據假說,我的程式碼很可能僅僅會使用ABC很短的時間。這個物件也許僅僅只是一個方法中的中間結果,並且隨著方法的返回這個物件就將變成垃圾了。大部分的新物件都是如此般地很快變成垃圾。

  然而,偶爾程式會建立一些很重要的,存活時間比較長的物件-例如web應用中的session變數或是配置項。

  通過頻繁的處理零代連結串列中的新物件,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新物件。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變數。

回到頂部

六、Ruby的代式垃圾回收

1、Ruby的自由鏈

  Ruby 2.1版本將會首次使用基於代的垃圾回收演算法!當自由鏈使用完之後,Ruby會標記你的程式仍然在使用的物件。

  

  從這張圖上我們可以看到有三個活躍的物件,因為指標n1、n2、n3仍然指向著它們。剩下的用白色矩形表示的物件即是垃圾。(實際情況會複雜得多,自由鏈可能會包含上千個物件,並且有複雜的引用指向關係,這裡的簡圖只是幫助我們瞭解Ruby的GC機制背後的簡單原理,而不會將我們陷入細節之中)

  Ruby會將垃圾物件移動回自由鏈中,這樣的話它們就能在程式申請新物件的時候被迴圈使用了。

  

2、Ruby2.1基於代的GC機制

  從2.1版本開始,Ruby的GC程式碼增加了一些附加步驟:它將留下來的活躍物件晉升(promote)到成熟代(mature generation)中。(在MRI的C原始碼中使用了old這個詞而不是mature),接下來的圖展示了兩個Ruby2.1物件代的概念圖:

  

  在左邊是一個跟自由鏈不相同的場景,我們可以看到垃圾物件是用白色表示的,剩下的是灰色的活躍物件。灰色的物件剛剛被標記。

  一旦“標記清除”過程結束,Ruby2.1將剩下的標記物件移動到成熟區:

  

  跟Python中使用三代來劃分不同,Ruby2.1只用了兩代,左邊是年輕的新一代物件,而右邊是成熟代的老物件。一旦Ruby2.1標記了物件一次,它就會被認為是成熟的。Ruby會打賭剩下的活躍物件在相當長的一段時間內不會很快變成垃圾物件。

  注意:Ruby2.1並不會真的在記憶體中拷貝物件,這些代表不同代的區域並不是由不同的實體記憶體區域構成。(有一些別的程式語言的GC實現或是Ruby的其他實現,可能會在物件晉升的時候採取拷貝的操作)。Ruby2.1的內部實現不會將在標記&清除過程中預先標記的物件包含在內。一旦一個物件已經被標記過一次了,那麼那將不會被包含在接下來的標記清除過程中。

  假定你的Ruby程式接著執行著,創造了更多新的,更年輕的物件。則GC的過程將會在新的一代中出現,如圖:

  

  

  如同Python那樣,Ruby的垃圾收集器將大部分精力都放在新一代的物件之上。它僅僅會將自上一次GC過程發生後建立的新的、年輕的物件包含在接下來的標記清除過程中。這是因為很多新物件很可能馬上就會變成垃圾(白色標記)。Ruby不會重複標記右邊的成熟物件。因為他們已經在一次GC過程中存活下來了,在相當長的一段時間內不會很快變成垃圾。因為只需要標記新物件,所以Ruby 的GC能夠執行得更快。它完全跳過了成熟物件,減少了程式碼等待GC完成的時間。

  偶然的Ruby會執行一次“全域性回收”,重標記(re-marking)並重清除(re-sweeping),這次包括所有的成熟物件。Ruby通過監控成熟物件的數目來確定何時執行全域性回收。當成熟物件的數目雙倍於上次全域性回收的數目時,Ruby會清理所有的標記並且將所有的物件都視為新物件。

3、白障

  假定你的程式碼建立了一個新的年輕的物件,並且將其作為一個已存在的成熟物件的子嗣加入。舉個例子,這種情況將會發生在,當你往一個已經存在了很長時間的陣列中增加了一個新值的時候:

  

  左邊的是新物件,而成熟的物件在右邊。在左邊標記過程已經識別出了5個新的物件目前仍然是活躍的(灰色)。但有兩個物件已經變成垃圾了(白色)。

  但是如何處理正中間這個新建物件?這是剛剛那個問題提到的物件,它是垃圾還是活躍物件呢?

  它是活躍物件,因為有一個從右邊成熟物件的引用指向它。但是我們前面說過已經被標記的成熟物件是不會被包含在標記清除過程中的(一直到全域性回收)。這意味著類似這種的新建物件會被錯誤的認為是垃圾而被釋放,從而造成資料丟失。

  Ruby2.1 通過監視成熟物件,觀察你的程式碼是否會新增一個從它們到新建物件的引用來克服這個問題。

  Ruby2.1 使用了一個名為白障(white barriers)的老式GC技術來監視成熟物件的變化 – 無論任意時刻當你添加了從一個物件指向另一個物件的引用時(無論是新建或是修改一個物件),白障就會被觸發。白障將會檢測是否源物件是一個成熟物件,如果是的話則將這個成熟物件新增到一個特殊的列表中。隨後,Ruby2.1會將這些滿足條件的成熟物件包括到下一次標記清除的範圍內,以防止新建物件被錯誤的標記為垃圾而清除。

  Ruby2.1 的白障實現相當複雜,主要是因為已有的C擴充套件並未包含這部分功能。Koichi Sasada以及Ruby的核心團隊使用了一個比較巧妙的方案來解決這個問題。如果想了解更多的內容,請閱讀這些相關材料:Koichi在EuRuKo 2013上的演講Koichi’s fascinating presentation。

回到頂部

七、總結

  乍眼一看,Ruby和Python的GC實現是截然不同的,Ruby使用John-MaCarthy的原生“標記並清除”演算法,而Python使用引用計數。但是仔細看來,可以發現Python使用了些許標記清除的思想來處理迴圈引用,而兩者同時以相似的方式使用基於代的垃圾回收演算法。Python劃分了三代,而Ruby只有兩代。

  這種相似性應該不會讓人感到意外。兩種程式語言都使用了幾十年前的電腦科學研究成果來進行設計,這些成果早在語言成型之前就已經被做出來了。我比較驚異的是當你掀開不同程式語言的表面而深入底層,你總能夠發現一些相似的基礎理念和演算法。現代程式語言應該感激那些六七十年代由麥卡錫等計算機先賢所作出的電腦科學開創性研究。