1. 程式人生 > >Python 中的垃圾回收機制--備忘

Python 中的垃圾回收機制--備忘

GC作為現代程式語言的自動記憶體管理機制,專注於兩件事:1. 找到記憶體中無用的垃圾資源 2. 清除這些垃圾並把記憶體讓出來給其他物件使用。GC徹底把程式設計師從資源管理的重擔中解放出來,讓他們有更多的時間放在業務邏輯上。但這並不意味著碼農就可以不去了解GC,畢竟多瞭解GC知識還是有利於我們寫出更健壯的程式碼。

引用計數

Python語言預設採用的垃圾收集機制是『引用計數法 Reference Counting』,該演算法最早George E. Collins在1960的時候首次提出,50年後的今天,該演算法依然被很多程式語言使用,『引用計數法』的原理是:每個物件維護一個ob_ref欄位,用來記錄該物件當前被引用的次數,每當新的引用指向該物件時,它的引用計數ob_ref加1,每當該物件的引用失效時計數ob_ref減1,一旦物件的引用計數為0,該物件立即被回收,物件佔用的記憶體空間將被釋放。它的缺點是需要額外的空間維護引用計數,這個問題是其次的,不過最主要的問題是它不能解決物件的“迴圈引用”,因此,也有很多語言比如Java並沒有採用該演算法做來垃圾的收集機制。

什麼是迴圈引用?A和B相互引用而再沒有外部引用A與B中的任何一個,它們的引用計數雖然都為1,但顯然應該被回收,例子:

          Python  
1 2 3 4 5 6 a = { } #物件A的引用計數為 1 b = { } #物件B的引用計數為 1 a['b'] = b  #B的引用計數增1 b['a'] = a  #A的引用計數增1 del a #A的引用減 1,最後A物件的引用為 1 del b #B的引用減 1, 最後B物件的引用為 1

recycle-refrence.jpg

在這個例子中程式執行完del語句後,A、B物件已經沒有任何引用指向這兩個物件,但是這兩個物件各包含一個對方物件的引用,雖然最後兩個物件都無法通過其它變數來引用這兩個物件了,這對GC來說就是兩個非活動物件或者說是垃圾物件,但是他們的引用計數並沒有減少到零。因此如果是使用引用計數法來管理這兩物件的話,他們並不會被回收,它會一直駐留在記憶體中,就會造成了記憶體洩漏(記憶體空間在使用完畢後未釋放)。為了解決物件的迴圈引用問題,Python引入了標記-清除和分代回收兩種GC機制。

標記清除

『標記清除(Mark—Sweep)』演算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收演算法。它分為兩個階段:第一階段是標記階段,GC會把所有的『活動物件』打上標記,第二階段是把那些沒有標記的物件『非活動物件』進行回收。那麼GC又是如何判斷哪些是活動物件哪些是非活動物件的呢?

物件之間通過引用(指標)連在一起,構成一個有向圖,物件構成這個有向圖的節點,而引用關係構成這個有向圖的邊。從根物件(root object)出發,沿著有向邊遍歷物件,可達的(reachable)物件標記為活動物件,不可達的物件就是要被清除的非活動物件。根物件就是全域性變數、呼叫棧、暫存器。

mark-sweepg

在上圖中,我們把小黑圈視為全域性變數,也就是把它作為root object,從小黑圈出發,物件1可直達,那麼它將被標記,物件2、3可間接到達也會被標記,而4和5不可達,那麼1、2、3就是活動物件,4和5是非活動物件會被GC回收。

標記清除演算法作為Python的輔助垃圾收集技術主要處理的是一些容器物件,比如list、dict、tuple,instance等,因為對於字串、數值物件是不可能造成迴圈引用問題。Python使用一個雙向連結串列將這些容器物件組織起來。不過,這種簡單粗暴的標記清除演算法也有明顯的缺點:清除非活動的物件前它必須順序掃描整個堆記憶體,哪怕只剩下小部分活動物件也要掃描所有物件。

分代回收

分代回收是一種以空間換時間的操作方式,Python將記憶體根據物件的存活時間劃分為不同的集合,每個集合稱為一個代,Python將記憶體分為了3“代”,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個連結串列,它們的垃圾收集頻率與物件的存活時間的增大而減小。新建立的物件都會分配在年輕代,年輕代連結串列的總數達到上限時,Python垃圾收集機制就會被觸發,把那些可以被回收的物件回收掉,而那些不會回收的物件就會被移到中年代去,依此類推,老年代中的物件是存活時間最久的物件,甚至是存活於整個系統的生命週期內。同時,分代回收是建立在標記清除技術基礎之上。分代回收同樣作為Python的輔助垃圾收集技術處理那些容器物件