Python:垃圾回收

BaKuai發表於2021-03-13
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 物件的引用圖如下:

Python:垃圾回收

3、引用環

兩個物件相互引用,即構成了所謂的引用環

a = []
b = [a]
a.append(b)

objgraph.show_refs([a,b], filename='a-b.png')
Python:垃圾回收

即使是單個物件,只需自己引用自己,也會構成引用環:

a = []
a.append(a)

objgraph.show_refs([a], filename='a-b.png')
Python:垃圾回收

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:

Python:垃圾回收

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


參考

相關文章