(2018-04-08.Python從Zero到One)一、python高級編程__1.3.2垃圾回收(二)

上一篇文章為:→1.3.1import垃圾回收

垃圾回收(二)

1. Garbage collection(GC垃圾回收)

現(xiàn)在的高級語言如java,c#等,都采用了垃圾收集機制,而不再是c,c++里用戶自己管理維護內存的方式。自己管理內存極其自由,可以任意申請內存,但如同一把雙刃劍,為大量內存泄露,懸空指針等bug埋下隱患。 對于一個字符串、列表、類甚至數(shù)值都是對象,且定位簡單易用的語言,自然不會讓用戶去處理如何分配回收內存的問題。 python里也同java一樣采用了垃圾收集機制,不過不一樣的是: python采用的是引用計數(shù)機制為主,標記-清除和分代收集兩種機制為輔的策略

引用計數(shù)機制:

python里每一個東西都是對象,它們的核心就是一個結構體:PyObject

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

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

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

當引用計數(shù)為0時,該對象生命就結束了。

引用計數(shù)機制的優(yōu)點:
  • 簡單
  • 實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。
引用計數(shù)機制的缺點:
  • 維護引用計數(shù)消耗資源

  • 循環(huán)引用

    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)
    
    

list1與list2相互引用,如果不存在其他對象對它們的引用,list1與list2的引用計數(shù)也仍然為1,所占用的內存永遠無法被回收,這將是致命的。 對于如今的強大硬件,缺點1尚可接受,但是循環(huán)引用導致內存泄露,注定python還將引入新的回收機制。(標記清除和分代收集)

2. 畫說 Ruby 與 Python 垃圾回收

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

2.1 應用程序那顆躍動的心

GC系統(tǒng)所承擔的工作遠比"垃圾回收"多得多。實際上,它們負責三個重要任務。它們

  • 為新生成的對象分配內存
  • 識別那些垃圾對象,并且
  • 從垃圾對象那回收內存。

如果將應用程序比作人的身體:所有你所寫的那些優(yōu)雅的代碼,業(yè)務邏輯,算法,應該就是大腦。以此類推,垃圾回收機制應該是那個身體器官呢?(我從RuPy聽眾那聽到了不少有趣的答案:腰子、白血球 :) )

我認為垃圾回收就是應用程序那顆躍動的心。像心臟為身體其他器官提供血液和營養(yǎng)物那樣,垃圾回收器為你的應該程序提供內存和對象。如果心臟停跳,過不了幾秒鐘人就完了。如果垃圾回收器停止工作或運行遲緩,像動脈阻塞,你的應用程序效率也會下降,直至最終死掉。

2.2 一個簡單的例子

運用實例一貫有助于理論的理解。下面是一個簡單類,分別用Python和Ruby寫成,我們今天就以此為例:

day13_其他知識-01.png

順便提一句,兩種語言的代碼竟能如此相像:Ruby 和 Python 在表達同一事物上真的只是略有不同。但是在這兩種語言的內部實現(xiàn)上是否也如此相似呢?

2.3 Ruby 的對象分配

當我們執(zhí)行上面的Node.new(1)時,Ruby到底做了什么?Ruby是如何為我們創(chuàng)建新的對象的呢? 出乎意料的是它做的非常少。實際上,早在代碼開始執(zhí)行前,Ruby就提前創(chuàng)建了成百上千個對象,并把它們串在鏈表上,名曰:可用列表。下圖所示為可用列表的概念圖:

day13_其他知識-02.png

想象一下每個白色方格上都標著一個"未使用預創(chuàng)建對象"。當我們調用 Node.new ,Ruby只需取一個預創(chuàng)建對象給我們使用即可:

day13_其他知識-03.png

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

如果我們再次調用 Node.new,Ruby將遞給我們另一個對象:

day13_其他知識-04.png

這個簡單的用鏈表來預分配對象的算法已經(jīng)發(fā)明了超過50年,而發(fā)明人這是赫赫有名的計算機科學家John McCarthy,一開始是用Lisp實現(xiàn)的。Lisp不僅是最早的函數(shù)式編程語言,在計算機科學領域也有許多創(chuàng)舉。其一就是利用垃圾回收機制自動化進行程序內存管理的概念。

標準版的Ruby,也就是眾所周知的"Matz's Ruby Interpreter"(MRI),所使用的GC算法與McCarthy在1960年的實現(xiàn)方式很類似。無論好壞,Ruby的垃圾回收機制已經(jīng)53歲高齡了。像Lisp一樣,Ruby預先創(chuàng)建一些對象,然后在你分配新對象或者變量的時候供你使用。

2.4 Python 的對象分配

我們已經(jīng)了解了Ruby預先創(chuàng)建對象并將它們存放在可用列表中。那Python又怎么樣呢?

盡管由于許多原因Python也使用可用列表(用來回收一些特定對象比如 list),但在為新對象和變量分配內存的方面Python和Ruby是不同的。

例如我們用Pyhon來創(chuàng)建一個Node對象:

day13_其他知識-05.png

與Ruby不同,當創(chuàng)建對象時Python立即向操作系統(tǒng)請求內存。(Python實際上實現(xiàn)了一套自己的內存分配系統(tǒng),在操作系統(tǒng)堆之上提供了一個抽象層。但是我今天不展開說了。)

當我們創(chuàng)建第二個對象的時候,再次像OS請求內存:

day13_其他知識-06.png

看起來夠簡單吧,在我們創(chuàng)建對象的時候,Python會花些時間為我們找到并分配內存。

2.5 Ruby 開發(fā)者住在凌亂的房間里

day13_其他知識-07.jpg

Ruby把無用的對象留在內存里,直到下一次GC執(zhí)行

回過來看Ruby。隨著我們創(chuàng)建越來越多的對象,Ruby會持續(xù)尋可用列表里取預創(chuàng)建對象給我們。因此,可用列表會逐漸變短:

day13_其他知識-08.png

...然后更短:

day13_其他知識-09.png

請注意我一直在為變量n1賦新值,Ruby把舊值留在原處。"ABC","JKL"和"MNO"三個Node實例還滯留在內存中。Ruby不會立即清除代碼中不再使用的舊對象!Ruby開發(fā)者們就像是住在一間凌亂的房間,地板上摞著衣服,要么洗碗池里都是臟盤子。作為一個Ruby程序員,無用的垃圾對象會一直環(huán)繞著你。

2.6 Python 開發(fā)者住在衛(wèi)生之家庭

day13_其他知識-10.jpg

用完的垃圾對象會立即被Python打掃干凈

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

day13_其他知識-11.png

在內部,創(chuàng)建一個對象時,Python總是在對象的C結構體里保存一個整數(shù),稱為 引用數(shù)。期初,Python將這個值設置為1:

day13_其他知識-12.png

值為1說明分別有個一個指針指向或是引用這三個對象。假如我們現(xiàn)在創(chuàng)建一個新的Node實例,JKL:

day13_其他知識-13.png

與之前一樣,Python設置JKL的引用數(shù)為1。然而,請注意由于我們改變了n1指向了JKL,不再指向ABC,Python就把ABC的引用數(shù)置為0了。 此刻,Python垃圾回收器立刻挺身而出!每當對象的引用數(shù)減為0,Python立即將其釋放,把內存還給操作系統(tǒng):

day13_其他知識-14.png

上面Python回收了ABC Node實例使用的內存。記住,Ruby棄舊對象原地于不顧,也不釋放它們的內存。

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

Python開發(fā)者工作在衛(wèi)生之家,你可以想象,有個患有輕度OCD(一種強迫癥)的室友一刻不停地跟在你身后打掃,你一放下臟碟子或杯子,有個家伙已經(jīng)準備好把它放進洗碗機了!

現(xiàn)在來看第二例子。加入我們讓n2引用n1:

day13_其他知識-15.png

上圖中左邊的DEF的引用數(shù)已經(jīng)被Python減少了,垃圾回收器會立即回收DEF實例。同時JKL的引用數(shù)已經(jīng)變?yōu)榱? ,因為n1和n2都指向它。

2.7 標記-清除

最終那間凌亂的房間充斥著垃圾,再不能歲月靜好了。在Ruby程序運行了一陣子以后,可用列表最終被用光光了:

day13_其他知識-16.png

此刻所有Ruby預創(chuàng)建對象都被程序用過了(它們都變灰了),可用列表里空空如也(沒有白格子了)。

此刻Ruby祭出另一McCarthy發(fā)明的算法,名曰:標記-清除。首先Ruby把程序停下來,Ruby用"地球停轉垃圾回收大法"。之后Ruby輪詢所有指針,變量和代碼產(chǎn)生別的引用對象和其他值。同時Ruby通過自身的虛擬機便利內部指針。標記出這些指針引用的每個對象。我在圖中使用M表示。

day13_其他知識-17.png

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

day13_其他知識-18.png

如果說被標記的對象是存活的,剩下的未被標記的對象只能是垃圾,這意味著我們的代碼不再會使用它了。我會在下圖中用白格子表示垃圾對象:

day13_其他知識-19.png

接下來Ruby清除這些無用的垃圾對象,把它們送回到可用列表中:

day13_其他知識-20.png

在內部這一切發(fā)生得迅雷不及掩耳,因為Ruby實際上不會吧對象從這拷貝到那。而是通過調整內部指針,將其指向一個新鏈表的方式,來將垃圾對象歸位到可用列表中的。

現(xiàn)在等到下回再創(chuàng)建對象的時候Ruby又可以把這些垃圾對象分給我們使用了。在Ruby里,對象們六道輪回,轉世投胎,享受多次人生。

2.8 標記-刪除 vs. 引用計數(shù)

乍一看,Python的GC算法貌似遠勝于Ruby的:寧舍潔宇而居穢室乎?為什么Ruby寧愿定期強制程序停止運行,也不使用Python的算法呢?

然而,引用計數(shù)并不像第一眼看上去那樣簡單。有許多原因使得不許多語言不像Python這樣使用引用計數(shù)GC算法:

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

第二點,它相對較慢。雖然Python隨著程序執(zhí)行GC很穩(wěn)?。ㄒ话雅K碟子放在洗碗盆里就開始洗啦),但這并不一定更快。Python不停地更新著眾多引用數(shù)值。特別是當你不再使用一個大數(shù)據(jù)結構的時候,比如一個包含很多元素的列表,Python可能必須一次性釋放大量對象。減少引用數(shù)就成了一項復雜的遞歸過程了。

最后,它不是總奏效的。引用計數(shù)不能處理環(huán)形數(shù)據(jù)結構--也就是含有循環(huán)引用的數(shù)據(jù)結構。

3. Python中的循環(huán)數(shù)據(jù)結構以及引用計數(shù)

3.1 循環(huán)引用

通過上篇,我們知道在Python中,每個對象都保存了一個稱為引用計數(shù)的整數(shù)值,來追蹤到底有多少引用指向了這個對象。無論何時,如果我們程序中的一個變量或其他對象引用了目標對象,Python將會增加這個計數(shù)值,而當程序停止使用這個對象,則Python會減少這個計數(shù)值。一旦計數(shù)值被減到零,Python將會釋放這個對象以及回收相關內存空間。

從六十年代開始,計算機科學界就面臨了一個嚴重的理論問題,那就是針對引用計數(shù)這種算法來說,如果一個數(shù)據(jù)結構引用了它自身,即如果這個數(shù)據(jù)結構是一個循環(huán)數(shù)據(jù)結構,那么某些引用計數(shù)值是肯定無法變成零的。為了更好地理解這個問題,讓我們舉個例子。下面的代碼展示了一些上周我們所用到的節(jié)點類:

day13_其他知識-21.jpg

我們有一個"構造器"(在Python中叫做 init ),在一個實例變量中存儲一個單獨的屬性。在類定義之后我們創(chuàng)建兩個節(jié)點,ABC以及DEF,在圖中為左邊的矩形框。兩個節(jié)點的引用計數(shù)都被初始化為1,因為各有兩個引用指向各個節(jié)點(n1和n2)。

現(xiàn)在,讓我們在節(jié)點中定義兩個附加的屬性,next以及prev:

day13_其他知識-22.jpg

跟Ruby不同的是,Python中你可以在代碼運行的時候動態(tài)定義實例變量或對象屬性。這看起來似乎有點像Ruby缺失了某些有趣的魔法。(聲明下我不是一個Python程序員,所以可能會存在一些命名方面的錯誤)。我們設置 n1.next 指向 n2,同時設置 n2.prev 指回 n1?,F(xiàn)在,我們的兩個節(jié)點使用循環(huán)引用的方式構成了一個雙向鏈表。同時請注意到 ABC 以及 DEF 的引用計數(shù)值已經(jīng)增加到了2。這里有兩個指針指向了每個節(jié)點:首先是 n1 以及 n2,其次就是 next 以及 prev。

現(xiàn)在,假定我們的程序不再使用這兩個節(jié)點了,我們將 n1 和 n2 都設置為null(Python中是None)。

day13_其他知識-23.jpg

好了,Python會像往常一樣將每個節(jié)點的引用計數(shù)減少到1。

3.2 在Python中的零代(Generation Zero)

請注意在以上剛剛說到的例子中,我們以一個不是很常見的情況結尾:我們有一個“孤島”或是一組未使用的、互相指向的對象,但是誰都沒有外部引用。換句話說,我們的程序不再使用這些節(jié)點對象了,所以我們希望Python的垃圾回收機制能夠足夠智能去釋放這些對象并回收它們占用的內存空間。但是這不可能,因為所有的引用計數(shù)都是1而不是0。Python的引用計數(shù)算法不能夠處理互相指向自己的對象。

這就是為什么Python要引入Generational GC算法的原因!正如Ruby使用一個鏈表(free list)來持續(xù)追蹤未使用的、自由的對象一樣,Python使用一種不同的鏈表來持續(xù)追蹤活躍的對象。而不將其稱之為“活躍列表”,Python的內部C代碼將其稱為零代(Generation Zero)。每次當你創(chuàng)建一個對象或其他什么值的時候,Python會將其加入零代鏈表:

day13_其他知識-24.jpg

從上邊可以看到當我們創(chuàng)建ABC節(jié)點的時候,Python將其加入零代鏈表。請注意到這并不是一個真正的列表,并不能直接在你的代碼中訪問,事實上這個鏈表是一個完全內部的Python運行時。 相似的,當我們創(chuàng)建DEF節(jié)點的時候,Python將其加入同樣的鏈表:

day13_其他知識-25.jpg

現(xiàn)在零代包含了兩個節(jié)點對象。(他還將包含Python創(chuàng)建的每個其他值,與一些Python自己使用的內部值。)

3.3 檢測循環(huán)引用

隨后,Python會循環(huán)遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據(jù)規(guī)則減掉其引用計數(shù)。在這個過程中,Python會一個接一個的統(tǒng)計內部引用的數(shù)量以防過早地釋放對象。

為了便于理解,來看一個例子:

day13_其他知識-27.jpg

從上面可以看到 ABC 和 DEF 節(jié)點包含的引用數(shù)為1.有三個其他的對象同時存在于零代鏈表中,藍色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。(接下來我們會看到,Python中同時存在另外兩個分別被稱為一代和二代的鏈表)。這些對象有著更高的引用計數(shù)因為它們正在被其他指針所指向著。

接下來你會看到Python的GC是如何處理零代鏈表的。

day13_其他知識-28.jpg

通過識別內部引用,Python能夠減少許多零代鏈表對象的引用計數(shù)。在上圖的第一行中你能夠看見ABC和DEF的引用計數(shù)已經(jīng)變?yōu)榱懔耍@意味著收集器可以釋放它們并回收內存空間了。剩下的活躍的對象則被移動到一個新的鏈表:一代鏈表。

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

Python中的GC閾值

Python什么時候會進行這個標記過程?隨著你的程序運行,Python解釋器保持對新創(chuàng)建的對象,以及因為引用計數(shù)為零而被釋放掉的對象的追蹤。從理論上說,這兩個值應該保持一致,因為程序新建的每個對象都應該最終被釋放掉。

當然,事實并非如此。因為循環(huán)引用的原因,并且因為你的程序使用了一些比其他對象存在時間更長的對象,從而被分配對象的計數(shù)值與被釋放對象的計數(shù)值之間的差異在逐漸增長。一旦這個差異累計超過某個閾值,則Python的收集機制就啟動了,并且觸發(fā)上邊所說到的零代算法,釋放“浮動的垃圾”,并且將剩下的對象移動到一代列表。

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

通過這種方法,你的代碼所長期使用的對象,那些你的代碼持續(xù)訪問的活躍對象,會從零代鏈表轉移到一代再轉移到二代。通過不同的閾值設置,Python可以在不同的時間間隔處理這些對象。Python處理零代最為頻繁,其次是一代然后才是二代。

弱代假說

來看看代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創(chuàng)建的,而一個來的對象則是經(jīng)過了幾個時間周期之后仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。

為什么要這么做?這種算法的根源來自于弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:首先是年親的對象通常死得也快,而老對象則很有可能存活更長的時間。

假定現(xiàn)在我用Python或是Ruby創(chuàng)建一個新對象:

day13_其他知識-29.jpg

根據(jù)假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,并且隨著方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創(chuàng)建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。

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


下一篇文章為:→1.3.3垃圾回收(三)-gc模塊
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 雖然是自己轉載的但是是真的好的一篇圖文并茂的對垃圾回收機制的講解!!! 先來個概述,第二部分的畫述才是厲害的。 G...
    東皇Amrzs閱讀 119,311評論 13 175
  • 一元類 1類也是對象 在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這一點仍然成...
    五行缺覺閱讀 1,163評論 0 1
  • 1.元類 1.1.1類也是對象 在大多數(shù)編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這...
    TENG書閱讀 1,419評論 0 3
  • 軟件項目管理中10大知識領域中(整合、范圍、時間、成本、質量、人力資源、溝通、風險、采購、干系人)項目資源...
    Smart熊大閱讀 5,108評論 1 8
  • 9月5號好種子的發(fā)芽 昨天和我朋友吃飯,之后幫朋友的朋友搬家,從來沒見過那位需要搬家的主人,主人回國了不回加拿大了...
    廢迪迪閱讀 207評論 0 1

友情鏈接更多精彩內容