Python垃圾回收(GC)三層心法,你瞭解到第幾層?

暴躁的熱心網友皮皮文發表於2018-06-29

垃圾回收機制應該是面試最常問的問題了,那麼Python中的垃圾回收機制(Garbage Collection)是怎麼解決的呢?我記得每一本python入門的書籍都會說python中請不要擔心記憶體洩漏這個 問題,那麼這個背後又是什麼原理,今天就來818。

Python中的GC演算法

分為下三點:引用計數/標記-清除/分代回收

·引用計數(主要)

剛開始學習Python的時候總是會有人告訴你,萬物皆物件是一大特色。在Python中每一個物件的核心就是一個結構體PyObject,它的內部有一個引用計數器(ob_refcnt)。

 typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;
複製程式碼
a=10
複製程式碼

引用計數的意思就是,一個物件在它剛被New出來呱呱(gugu不是guagua)墜地的時候因為被New方法引用了所以他的引用計數就是1,如果它被引用(也就是在之前的基礎上 例如:b=a,被丟入函式列表等等被引用就會在引用計數上加1),如果引用它的物件被刪除的時候(在之前的基礎上DEL b)那麼它的引用計數就會減少一一直到當它的引用計數變為0的時候,垃圾回收機制就會找上門做掉它(回收),腦補一下 :開門我是查水錶的。

優點/缺點:

因為引用計數是GC主要方法,來看一下優缺點。

優:

簡單,實時性(一旦為零就不跟你多BB,做掉)

缺:

· 維護性高(簡單實時,但是額外佔用了一部分資源,雖然邏輯簡單,但是麻煩。好比你吃草莓,吃一次洗一下手,而不是吃完洗手。)

· 不能解決的情況:--->迴圈引用(如下):

a=[1,2]
b=[2,3]
a.append(b)
b.append(a)
DEL a
DEL b
複製程式碼

說實話感覺還有點像死鎖的問題,這種問題出現在可以迴圈的結構中List Dict Object等等,如上程式碼a、b間的引用都為1,而a、b被引用的物件刪除後都各自減去1(所以他們各自的引用計數還是1),這時候就尷尬了啊,都是1就有了免死金牌(一直是1不會變化了)。這樣的情況單單靠引用計數就無法解決了。 也為我們引入了下面的主題 標記-清除

·標記-清除: 標記清除就是用來解決迴圈引用的問題的只有容器物件才會出現引用迴圈,比如列表、字典、類、元組。 首先,為了追蹤容器物件,需要每個容器物件維護兩個額外的指標, 用來將容器物件組成一個連結串列,指標分別指向前後兩個容器物件,方便插入和刪除操作。試想一下,現在有兩種情況:

A:

a=[1,3]
b=[2,4]
a.append(b)
b.append(a)
del a
del b
複製程式碼

B:

a=[1,3]
b=[2,4]
a.append(b)
b.append(a)
del a
複製程式碼

Okay,現在開始說正題。在標記-清除演算法中,有兩個集中營,一個是root連結串列(root object),另外一個是unreachable連結串列

· 對於情景A,原來再未執行DEL語句的時候,a,b的引用計數都為2(init+append=2),但是在DEL執行完以後,a,b引用次數互相減1。a,b陷入迴圈引用的圈子中,然後標記-清除演算法開始出來做事,找到其中一端a,開始拆這個a,b的引用環(我們從A出發,因為它有一個對B的引用,則將B的引用計數減1;然後順著引用達到B,因為B有一個對A的引用,同樣將A的引用減1,這樣,就完成了迴圈引用物件間環摘除。),去掉以後發現,a,b迴圈引用變為了0,所以a,b就被處理到unreachable連結串列中直接被做掉

· 對於情景B,簡單一看那b取環後引用計數還為1,但是a取環,就為0了。這個時候a已經進入unreachable連結串列中,已經被判為死刑了,但是這個時候,root連結串列中有b。如果a被做掉,那世界上還有什麼正義... ,在root連結串列中的b會被進行引用檢測引用了a,如果a被做掉了,那麼b就...涼涼,一審完事,二審a無罪,所以被拉到了root連結串列中。

QA: 為什麼要搞這兩個連結串列

之所以要剖成兩個連結串列,是基於這樣的一種考慮:現在的unreachable可能存在被root連結串列中的物件,直接或間接引用的物件,這些物件是不能被回收的,一旦在標記的過程中,發現這樣的物件,就將其從unreachable連結串列中移到root連結串列中;當完成標記後,unreachable連結串列中剩下的所有物件就是名副其實的垃圾物件了,接下來的垃圾回收只需限制在unreachable連結串列中即可。

分代回收:

瞭解分類回收,首先要了解一下,GC的閾值,所謂閾值就是一個臨界點的值。隨著你的程式執行,Python直譯器保持對新建立的物件,以及因為引用計數為零而被釋放掉的物件的追蹤。從理論上說,建立==釋放數量應該是這樣子。但是如果存在迴圈引用的話,肯定是建立>釋放數量,當建立數與釋放數量的差值達到規定的閾值的時候,噹噹噹當~分代回收機制就登場啦。

垃圾回收=垃圾檢測+釋放

分代回收思想將物件分為三代(generation 0,1,2),0代表幼年物件,1代表青年物件,2代表老年物件。根據弱代假說(越年輕的物件越容易死掉,老的物件通常會存活更久。) 新生的物件被放入0代,如果該物件在第0代的一次gc垃圾回收中活了下來,那麼它就被放到第1代裡面(它就升級了)。如果第1代裡面的物件在第1代的一次gc垃圾回收中活了下來,它就被放到第2代裡面。gc.set_threshold(threshold0[,threshold1[,threshold2]])設定gc每一代垃圾回收所觸發的閾值。從上一次第0代gc後,如果分配物件的個數減去釋放物件的個數大於threshold0,那麼就會對第0代中的物件進行gc垃圾回收檢查。 從上一次第1代gc後,如過第0代被gc垃圾回收的次數大於threshold1,那麼就會對第1代中的物件進行gc垃圾回收檢查。同樣,從上一次第2代gc後,如過第1代被gc垃圾回收的次數大於threshold2,那麼就會對第2代中的物件進行gc垃圾回收檢查。

這算是簡單的講完了Python的GC,打完收工。

相關文章