有很多不同的方法來實現垃圾回收,例如跟蹤,引用計數,轉義分析,時間戳和心跳訊號等。不同的語言依賴於不同的垃圾回收實現,例如,有些將其與編譯器和執行時系統整合在一起。而其他語言則可能需要事後設定,甚至可能需要重新編譯。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的物件及這些物件引用的物件,以及更下游的物件會被保留,而引用環中的物件會被回收。