Python:垃圾回收

有很多不同的方法來實現垃圾回收,例如跟蹤,引用計數,轉義分析,時間戳和心跳訊號等。不同的語言依賴於不同的垃圾回收實現,例如,有些將其與編譯器和運行時系統集成在一起。而其他語言則可能需要事後設置,甚至可能需要重新編譯。Python中垃圾收集器使用基於引用計數的方法。它在程式執行期間運行,並在對象的引用計數達到0時開始工作。
1、引用管理
首先,記憶體管理是基於引用的管理。我們知道Python中,引用與對象是分離的,一個對象可以有多個引用,而每個對象都存有指向自己的引用計數。可以使用標準庫sys
查看某個對象的引用計數:
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) # 列印2
b = a
print(getrefcount(a)) # 列印3
由於調用
getrefcount()
時又創建了一次引用,所以列印的引用計數會比實際多一個。
2、對象引用對象
Python中對象會引用別的對象,而容器對象的引用會構成很複雜的拓撲結構:
l = [1,2,3]
d = {"k": l}
y = [l, d]
z = [y,(l,y)]
使用 objgraph
包可以繪製引用關係:
...
import objgraph
objgraph.show_refs([z], filename='sample-graph.png')
繪製的 z 對象的引用圖如下:

3、引用環
兩個對象相互引用,即構成了所謂的引用環:
a = []
b = [a]
a.append(b)
objgraph.show_refs([a,b], filename='a-b.png')

即使是單個對象,只需自己引用自己,也會構成引用環:
a = []
a.append(a)
objgraph.show_refs([a], filename='a-b.png')

del
關鍵字除了可以刪除容器中的元素,還可以刪除某個引用。
4、垃圾回收
CPython中的記憶體管理和垃圾回收有兩個策略:
-
引用計數
-
分代回收
4.1 引用計數
CPython中主要的垃圾收集機制是通過引用計數,且引用計數無法被禁用,而後面談到的分代回收策略則可以禁止。
原理上,Python的某個對象的引用計數變為0時,就要成為被回收的垃圾了。例如:
a = [1,2,3]
del a
當垃圾回收啟動時,Python掃描到這個引用計數為0的對象,會將其所佔據的記憶體清空。而垃圾回收是個費時費力的事,垃圾回收期間Python不能進行其他任務。頻繁的垃圾回收會大大降低Python的效率,所以Python只會在特定條件下啟動垃圾回收。Python運行時,會記錄其中分配對象和取消分配對象的次數,當兩者差值高於某個閾值,垃圾回收才會啟動。
有人說引用計數是一個窮人的垃圾收集器。它確實有一些缺點,包括無法檢測到循環引用。但是,引用計數的優點是,你可以在沒有引用的情況下立即刪除該對象。
4.2 分代回收
除了上面這種實時的,自動的基於引用計數的垃圾回收實現方法,Python還同時採用分代回收策略,這一次略的基本假設是,存活時間越久的對象,越不可能在以後成為垃圾。Python將所有對象分為三代,所有新建對象都是0代,如果經過一次掃描沒被回收即成為了1代,以此類推。
Python的基於引用計數的方法是自動的,並且是實時發生的,而分代垃圾回收模組的操作是周期性的,可以手動調用,常用API:
-
get_shreshold()
方法可以查看觸發垃圾收集的閾值: -
gc.get_count()
方法可以查看記憶體中當前存在的各代對象數量 -
gc.set_threshold(
)方法可以更改觸發垃圾收集的閾值
>>> import gc
>>> print(gc.get_threshold()) # (700, 10, 10)
>>> gc.set_threshold(700.10,5) # 2代垃圾回收會更頻繁
>>> gc.collect() # 手動觸發垃圾回收
對於每一代,垃圾收集器模組都有一個閾值對象。如果對象數超過該閾值,則垃圾收集器將觸發收集過程,在該過程中倖存下來的對象會被歸為下一代。默認情況下,Python對於最年輕的一代的閾值為700,對於兩個較老的一代中的每個閾值為10。
引用環的回收
分代回收策略可以可以檢測和解決引用環問題,在Python 1.5中引入了循環檢測演算法,它跟蹤容器對象,即可以容納其他對象(例如列表,字典,類等)的對象,因為只有它們才能創建此這種引用環。
循環檢測演算法的基本原理是:Python會複製每個對象的引用計數,記為gc_ref
。假設每個對象為i
,該對象的計數為gc_ref_i
。Python會遍歷所有的對象i
,對於每個對象i
所引用的對象j
,將j
的gc_ref_j
減1:

遍歷後,gc_ref不為0的對象及這些對象引用的對象,以及更下游的對象會被保留,而引用環中的對象會被回收。
參考
-
Python深入06 Python的記憶體管理,by Vamei