Python的垃圾回收機制深入分析--迴圈引用
一、概述:
Python的GC模組主要運用了“引用計數”(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,還可以通過“標記-清除”(mark and sweep)解決容器物件可能產生的迴圈引用的問題。通過“分代回收”(generation collection)以空間換取時間來進一步提高垃圾回收的效率。
二、引用計數
在Python中,大多數物件的生命週期都是通過物件的引用計數來管理的。從廣義上來講,引用計數也是一種垃圾收集機制,而且也是一種最直觀,最簡單的垃圾收集技術。
原理:當一個物件的引用被建立或者複製時,物件的引用計數加1;當一個物件的引用被銷燬時,物件的引用計數減1;當物件的引用計數減少為0時,就意味著物件已經沒有被任何人使用了,可以將其所佔用的記憶體釋放了。
雖然引用計數必須在每次分配和釋放記憶體的時候加入管理引用計數的動作,然而與其他主流的垃圾收集技術相比,引用計數有一個最大的有點,即“實時性”,任何記憶體,一旦沒有指向它的引用,就會立即被回收。而其他的垃圾收集計數必須在某種特殊條件下(比如記憶體分配失敗)才能進行無效記憶體的回收。
引用計數機制執行效率問題:引用計數機制所帶來的維護引用計數的額外操作與Python執行中所進行的記憶體分配和釋放,引用賦值的次數是成正比的。而這點相比其他主流的垃圾回收機制,比如“標記-清除”,“停止-複製”,是一個弱點,因為這些技術所帶來的額外操作基本上只是與待回收的記憶體數量有關。
如果說執行效率還僅僅是引用計數機制的一個軟肋的話,那麼很不幸,引用計數機制還存在著一個致命的弱點,正是由於這個弱點,使得俠義的垃圾收集從來沒有將引用計數包含在內,能引發出這個致命的弱點就是迴圈引用(也稱交叉引用)。
問題說明:
迴圈引用可以使一組物件的引用計數不為0,然而這些物件實際上並沒有被任何外部物件所引用,它們之間只是相互引用。這意味著不會再有人使用這組物件,應該回收這組物件所佔用的記憶體空間,然後由於相互引用的存在,每一個物件的引用計數都不為0,因此這些物件所佔用的記憶體永遠不會被釋放。比如:
1 2 3 4 5 6 7 8 |
a
=
[]
b
=
[]
a.append(b)
b.append(a)
print
a
[[[…]]]
print
b
[[[…]]]
|
這一點是致命的,這與手動進行記憶體管理所產生的記憶體洩露毫無區別。
要解決這個問題,Python引入了其他的垃圾收集機制來彌補引用計數的缺陷:“標記-清除”,“分代回收”兩種收集技術。
三、標記-清除
“標記-清除”是為了解決迴圈引用的問題。可以包含其他物件引用的容器物件(比如:list,set,dict,class,instance)都可能產生迴圈引用。
我們必須承認一個事實,如果兩個物件的引用計數都為1,但是僅僅存在他們之間的迴圈引用,那麼這兩個物件都是需要被回收的,也就是說,它們的引用計數雖然表現為非0,但實際上有效的引用計數為0。我們必須先將迴圈引用摘掉,那麼這兩個物件的有效計數就現身了。假設兩個物件為A、B,我們從A出發,因為它有一個對B的引用,則將B的引用計數減1;然後順著引用達到B,因為B有一個對A的引用,同樣將A的引用減1,這樣,就完成了迴圈引用物件間環摘除。
但是這樣就有一個問題,假設物件A有一個物件引用C,而C沒有引用A,如果將C計數引用減1,而最後A並沒有被回收,顯然,我們錯誤的將C的引用計數減1,這將導致在未來的某個時刻出現一個對C的懸空引用。這就要求我們必須在A沒有被刪除的情況下復原C的引用計數,如果採用這樣的方案,那麼維護引用計數的複雜度將成倍增加。
原理:“標記-清除”採用了更好的做法,我們並不改動真實的引用計數,而是將集合中物件的引用計數複製一份副本,改動該物件引用的副本。對於副本做任何的改動,都不會影響到物件生命走起的維護。
這個計數副本的唯一作用是尋找root object集合(該集合中的物件是不能被回收的)。當成功尋找到root object集合之後,首先將現在的記憶體連結串列一分為二,一條連結串列中維護root object集合,成為root連結串列,而另外一條連結串列中維護剩下的物件,成為unreachable連結串列。之所以要剖成兩個連結串列,是基於這樣的一種考慮:現在的unreachable可能存在被root連結串列中的物件,直接或間接引用的物件,這些物件是不能被回收的,一旦在標記的過程中,發現這樣的物件,就將其從unreachable連結串列中移到root連結串列中;當完成標記後,unreachable連結串列中剩下的所有物件就是名副其實的垃圾物件了,接下來的垃圾回收只需限制在unreachable連結串列中即可。
四、分代回收
背景:分代的垃圾收集技術是在上個世紀80年代初發展起來的一種垃圾收集機制,一系列的研究表明:無論使用何種語言開發,無論開發的是何種型別,何種規模的程式,都存在這樣一點相同之處。即:一定比例的記憶體塊的生存週期都比較短,通常是幾百萬條機器指令的時間,而剩下的記憶體塊,起生存週期比較長,甚至會從程式開始一直持續到程式結束。
從前面“標記-清除”這樣的垃圾收集機制來看,這種垃圾收集機制所帶來的額外操作實際上與系統中總的記憶體塊的數量是相關的,當需要回收的記憶體塊越多時,垃圾檢測帶來的額外操作就越多,而垃圾回收帶來的額外操作就越少;反之,當需回收的記憶體塊越少時,垃圾檢測就將比垃圾回收帶來更少的額外操作。為了提高垃圾收集的效率,採用“空間換時間的策略”。
原理:將系統中的所有記憶體塊根據其存活時間劃分為不同的集合,每一個集合就成為一個“代”,垃圾收集的頻率隨著“代”的存活時間的增大而減小。也就是說,活得越長的物件,就越不可能是垃圾,就應該減少對它的垃圾收集頻率。那麼如何來衡量這個存活時間:通常是利用幾次垃圾收集動作來衡量,如果一個物件經過的垃圾收集次數越多,可以得出:該物件存活時間就越長。
舉例說明:
當某些記憶體塊M經過了3次垃圾收集的清洗之後還存活時,我們就將記憶體塊M劃到一個集合A中去,而新分配的記憶體都劃分到集合B中去。當垃圾收集開始工作時,大多數情況都只對集合B進行垃圾回收,而對集合A進行垃圾回收要隔相當長一段時間後才進行,這就使得垃圾收集機制需要處理的記憶體少了,效率自然就提高了。在這個過程中,集合B中的某些記憶體塊由於存活時間長而會被轉移到集合A中,當然,集合A中實際上也存在一些垃圾,這些垃圾的回收會因為這種分代的機制而被延遲。
在Python中,總共有3“代”,也就是Python實際上維護了3條連結串列。具體可以檢視Python原始碼詳細瞭解。