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,將jgc_ref_j減1:

遍歷後,gc_ref不為0的對象及這些對象引用的對象,以及更下游的對象會被保留,而引用環中的對象會被回收。

參考

Tags: