1. 程式人生 > 其它 >Python 的垃圾回收

Python 的垃圾回收

首先介紹兩個畫圖的工具:objgraph 庫和線上繪圖網站 draw.io。具體的使用以後再寫。

垃圾回收
1.引用計數

Python 中,每個物件都有存有指向該物件的引用總數,即:引用計數(reference count);
可以使用 sys 包中的 getrefcount(),來檢視某個物件的引用計數;
需要注意的是,當使用某個引用作為引數,傳遞給 getrefcount() 時,引數實際上建立了一個臨時的引用。因此,getrefcount() 所得到的結果,會比期望的多 1 ;

from sys import getrefcount

a = [1, 2, 3]
print(getrefcount(a))  # 2

b = a
print(getrefcount(b))  # 3

Python的一個容器物件(container),比如表、詞典等,可以包含多個物件。實際上,容器物件中包含的並不是元素物件本身,是指向各個元素物件的引用;
即使是 a = 1 這一賦值方式,實際上是讓詞典的一個鍵值 "a" 的元素引用整數物件 1。該詞典物件用於記錄所有的全域性引用。該詞典引用了整數物件 1。我們可以通過內建函式 globals() 來檢視該詞典。

容器物件的引用可能構成很複雜的拓撲結構。我們可以用[^objgraph ]包來繪製其引用關係,比如:

import objgraph

x = [1, 2, 3]
y = [x, dict(key1=x)]
z = [y, (x, y)]

objgraph.show_refs([z])

兩個物件可能相互引用,從而構成所謂的引用環(reference cycle):

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

即使是一個物件,只需要自己引用自己,也能構成引用環:

c = []
c.append(c)
print(getrefcount(c))
objgraph.show_refs([c])

某個物件的引用計數可能減少。比如,使用 del 關鍵字刪除某個引用,del a
當 Python 中的物件越來越多,它們將佔據越來越大的記憶體,並在適當的時候啟動垃圾回收(garbage collection),將沒用的物件清除;

2.引用計數為 0

當 Python 的某個物件的引用計數降為 0 時,說明沒有任何引用指向該物件,該物件就成為要被回收的垃圾了;
然而,垃圾回收時,Python 不能進行其它的任務,頻繁的垃圾回收將大大降低 Python 的工作效率;
如果記憶體中的物件不多,就沒有必要總啟動垃圾回收。所以,Python 只會在特定條件下,自動啟動垃圾回收:
當 Python 執行時,會記錄其中分配物件(object allocation)和取消分配物件(object deallocation)的次數。當兩者的差值高於某個閾值時,垃圾回收才會啟動。

可以通過 gc 模組的 get_threshold() 方法,檢視該閾值:

import gc

gc.get_threshold()  # (700, 10, 10),兩個10是與分代回收相關的閾值,700 是垃圾回收啟動閾值;
gc.set_threshold(800, 10, 5)  # 重新設定垃圾回收的相關閾值
gc.collect()  # 手動啟動垃圾回收, gc.collect()
3.分代回收

Python 同時採用了分代(generation)回收的策略。這一策略的基本假設是:
存活時間越久的物件,越不可能在後面的程式中變成垃圾。我們的程式往往會產生大量的物件,許多物件很快產生和消失,但也有一些物件長期被使用。出於信任和效率,對於這樣一些“長壽”物件,我們相信它們的用處,所以減少在垃圾回收中掃描它們的頻率。

Python 將所有的物件分為 0,1,2 三代。所有的新建物件都是 0 代物件。當某一代物件經歷過垃圾回收,依然存活,那麼它就被歸入下一代物件。垃圾回收啟動時,一定會掃描所有的 0 代物件。如果 0 代經過一定次數垃圾回收,那麼就啟動對 0 代和 1代 的掃描清理。當 1 代也經歷了一定次數的垃圾回收後,那麼會啟動對 0,1,2,即對所有物件進行掃描。

(700, 10, 10)中的兩個 10 代表:
每 10 次 0 代垃圾回收,會有 1 次 1 代的垃圾回收;每 10 次 1 代的垃圾回收,會有 1 次的 2 代垃圾回收;

4.孤立的引用環--標記清除法

引用環的存在會給垃圾回收機制帶來很大的困難,可能構成無法使用,但引用計數不為 0 的一些物件:

"""
	下面建立了兩個列表物件,並引用對方,構成一個引用環;
	刪除了a,b引用之後,這兩個物件不可能再從程式中呼叫,就沒有什麼用處了;
	但是由於引用環的存在,這兩個物件的引用計數都沒有降到0,不會被垃圾回收;
"""
a = []
b = [a]
a.append(b)

del a
del b

為了回收這樣的引用環,Python 會複製每個物件的引用計數,可以記為 gc_ref。假設,每個物件 i 的引用計數為 gc_ref_i。Python會遍歷所有的物件 i。對於每個物件 i 引用的物件 j,將相應的 gc_ref_j - 1;

在結束遍歷後,gc_ref 不為 0 的物件,和這些物件引用的物件,以及繼續更下游引用的物件,需要被保留,而其它的物件則被垃圾回收;稱之為“標記清除法”.

5.dot 解析網站

objgraph.show_refs() 生成的 dot 檔案解析網站 https://onlineconvertfree.com/zh/

import objgraph
a = [1,2,3]
b = [4,5,6]

a.append(b)
b.append(a)

objgraph.show_refs(a)
objgraph.show_refs([a])
objgraph.show_refs([b])

[^objgraph ]: https://onlineconvertfree.com/zh/ dot 轉 png