Python對象循環引用
我們來介紹一下 Python 是采用何種途徑解決循環引用問題的。
循環引用垃圾回收算法
上圖中,表示的是對象之間的引用關系,從自對象指向他對象的引用用黑色箭頭表示。每個對象里都有計數器。而圖中右側部分可以很清晰的看到是循環引用的垃圾對象。
上圖,將每個對象的引用計數器復制到自己的另一個存儲空間中。
上圖其實和圖二(圖片左上角)沒什么區別,只不過更清晰了。因為對象本來就是由對象鏈表連接的。只不過是把對象鏈表畫了出來。
上圖中,將新復制的計數器都進行了減量的操作。先不要管為什么,繼續往下看。
但是可以看到,由根直接引用的對象中,新復制的計數器並沒有減量。
以上操作執行完畢后,再把對象分為可能到達的對象鏈表和不可能到達的對象鏈表。
之后將具備如下條件的對象連接到“可能到達對象的鏈表”。
- 經過 (4) 的減量操作后計數器值大於等於 1。
- 有從活動對象的引用。
再將具備如下條件的對象連接到“不可能到達對象的鏈表”。
- 經過 (4) 的減量操作后計數器值為 0
- 沒有從活動對象的引用
現在上圖顯示的就是垃圾對象鏈表和活動對象的鏈表了。接下來的步驟就是釋放不可能到達的對象,再把可能到達的對象連接到對象鏈表。
這樣,Python中只要將“部分標記-清除算法”稍加變形,就解決了循環引用問題。
容器對象
並不是所有的Python對象都會發生循環引用。有些對象可能保留了指向其他對象的引用,這些對象可能引起循環引用。
這些可以保留了指向其他對象的引用的對象 就被稱為容器對象。具有代表性的就是元組,字典,列表。
非容器對象有字符串和數值等。這些對象不能保留指向其他對象的引用。
容器對象中都被分配了用於循環引用垃圾回收的頭結構體
這個用於循環引用垃圾回收的頭包含以下信息:
- 用於容器對象雙向鏈表的成員。
- 用於復制引用計數器的成員。
定義如下:Include/objimpl.h
typedef union _gc_head {
struct {
union _gc_head *gc_next; /* 用於雙向鏈表 */
union _gc_head *gc_prev; /* 用於雙向鏈表 */
Py_ssize_t gc_refs; /* 用於復制 */
} gc;
long double dummy;
} PyGC_Head;
結構體 PyGC_Head 里面是結構體 gc 和成員 dummy 的聯合體。
在這里成員 dummy 起到了一定的作用:即使結構體 gc 的大小為9字節這樣不上不下的 數值,它也會將整個結構體 PyGC_Head 的大小對齊為 long double 型。因為結構體 gc 的大 小不太可能變成這樣不上不下的數值,所以事實上 dummy 起到了一個以防萬一的作用。
生成容器對象
在生成容器對象時,必須分配用於循環引用垃圾回收的頭,在這里由 _PyObject_GC_Malloc() 函數來執行分配頭的操作。這個函數是負責分配所 有容器對象的函數。
Modules/gcmodule.c: _PyObject_GC_Malloc():只有分配頭的部分
PyObject * _PyObject_GC_Malloc(size_t basicsize)
{
PyObject *op;
PyGC_Head *g;
g = (PyGC_Head *)PyObject_MALLOC(
sizeof(PyGC_Head) + basicsize);
g->gc.gc_refs = GC_UNTRACKED;
/* 開始進行循環引用垃圾回收:后述 */
op = FROM_GC(g);
return op; }
-
1.首先分配對象,於此同時分配了結構體PyGC_Head。
-
2.將GC_UNTRACKED存入用於循環引用垃圾回收的頭內成員gc_refs中。當出現這個標志的時候,GC會認為這個容器對象沒有被連接到對象鏈表。
define _PyGC_REFS_UNTRACKED (-2)
這個_PyGC_REFS_UNTRACKED是GC_UNTRACKED的別名。gc_ref是用於復制對象的引用計數器的成員,不過它是用負值作為標識的。再次說明這里這樣做,補另建立對象做這件事情是為了減輕負擔。
-
3.最后調用了宏FROM_GC()返回結果。
define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
這個宏會偏移用於循環引用垃圾回收的頭的長度,返回正確的對象地址。這是因為這項操作,調用方才不用區別對待有循環引用垃圾回收頭的容器對象和其他對象。
如果結構體 PyGC_Head 的大小沒有對齊,FROM_GC() 返回的地址就是沒有被對齊的不上 不下的值,因此需要按合適的大小對齊結構體 PyGC_Head 的大小。
追蹤容器對象
為了釋放循環引用,需要將容器對象用對象鏈表連接(雙向)。再生成容器對象之后就要馬上連接鏈表。下面以字典對象為例:
PyObject * PyDict_New(void)
{
register PyDictObject *mp;
/* 生成對象的操作 */
_PyObject_GC_TRACK(mp);
return (PyObject *)mp;
}
_PyObject_GC_TRACK() 負責連接鏈表的操作。
#define _PyObject_GC_TRACK(o) do {
PyGC_Head *g = _Py_AS_GC(o);
g->gc.gc_refs = _PyGC_REFS_REACHABLE;
g->gc.gc_next = _PyGC_generation0;
g->gc.gc_prev = _PyGC_generation0->gc.gc_prev; g->gc.gc_prev->gc.gc_next = g;
_PyGC_generation0->gc.gc_prev = g;
} while (0);
這個宏里有一點需要注意的,那就是do--while循環。這里不是為了循環,而是寫宏的技巧。讀代碼時可以將其無視。
我們來看看宏內部_Py_AS_GC()的定義如下:#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)
- 首先從對象的開頭,將頭地址偏移相應的大小,取出循環引用垃圾回收的頭。
- _PyGC_REFS_REACHABLE 這個標志存入成員 gc_refs 中。標志程序可能到達對象的意思。
- 最后全局性容器對象鏈表(擁有所有容器對象的全局性容器),把對象連接到這個鏈表。
這樣一來就把所有容器對象都連接到了作為容器對象鏈表的雙向鏈表中。循環引用垃圾回收就是用這個容器對象鏈表來釋放循環引用對象的。
結束追蹤容器對象
通過引用計數法釋放容器對象之前,要把容器對象從容器對象鏈表中取出。因為呢沒有必要去追蹤已經釋放了的對象,所以這么做是理所應當的。下面以字典對象為例釋放字典的函數。
- 使用PyObject_GC_UnTrack() 函數來執行結束追蹤對象的操作
- IS_TRACKED()包含在 PyObject_GC_UnTrack(),判斷對象是不是正在追蹤的對象
- AS_GC() 是之前講過的宏 _Py_AS_GC() 的別名,包含在IS_TRACKED()里。用於判斷對象是不是正在追蹤,如果是就結束追蹤。
- IS_TRACKED()包含在 PyObject_GC_UnTrack(),判斷對象是不是正在追蹤的對象
- _PyGC_REFS_UNTRACKED 這里只是將追蹤對象以外的標志存入成員gc_refs,並從容 器對象鏈表中去除而已。
大多數情況下是通過引用計數法的減量操作來釋放容器對象的,因為循環引用垃圾回收釋放的知識具有循環引用關系的對象群,所以數量並沒有那么多。
分代容器對象鏈表
容器對象鏈表分為三代。循環引用垃圾回收事實上是分帶垃圾回收。
系統通過下面的結構體來管理各代容器對象鏈表。
struct gc_generation {
PyGC_Head head;
int threshold; /* 開始GC的閾值 */
int count; /* 該代的對象數 */
};
- 首先將容器對象連接到成員head
- 設置threshold閾值
- 當count大於閾值的時候啟動GC。
- 不同的代閾值是不同的,count也是不同的。
一開始所有容器對象都連接0代對象。之后只有經過循環引用垃圾回收的對象活下來一定次數才能夠晉升。
何時執行循環引用垃圾回收
在生成容器對象的時候執行循環引用垃圾回收。代碼如下:
Modules/gcmodule.c
PyObject * _PyObject_GC_Malloc(size_t basicsize)
{
PyObject *op;
PyGC_Head *g;
/* 生成對象的操作 */
/* 對分配的對象數進行增量操作 */
generations[0].count++;
if (generations[0].count > generations[0].threshold && enabled && generations[0].threshold && !collecting && !PyErr_Occurred()) {
collecting = 1;
collect_generations();
collecting = 0;
} op = FROM_GC(g);
return op;
}
- 先進性對0代成員count執行增量操作。generations[0].count++;
- 接下來檢測0代的count有沒有超過閾值。
- 接着確認全局變量enabled是0以外的數值。只有在用戶不想運行循環引用垃圾回收的時候,才為0.通過python就可進行設置。
- 確認threshold不為0.
- 確認循環引用垃圾回收是否正在執行。
- 最后執行PyErr_Occurred()函數檢測有沒有發生異常。
- 如果檢測全部合格,就開始執行循環引用的垃圾回收。
- 在循環引用的垃圾回收時,將全局變量collecting設置1,調用collect_generations()函數。這就是調用循環引用垃圾回收的部分。
Modules/gcmodule.c
static Py_ssize_t collect_generations(void)
{
int i;
Py_ssize_t n = 0;
for (i = NUM_GENERATIONS-1; i >= 0; i--) {
if (generations[i].count > generations[i].threshold) {
n = collect(i); /* 執行循環引用垃圾回收! */ break;
}
}
return n;
}
在這里檢查各代的計數器和閾值,對超過閾值的代執行GC,這樣一來循環引用垃圾回 收的所有內容就都裝入了程序調用的 collect() 函數里。
循環引用的垃圾回收
來看一下collect()Modules/gcmodule.c
static Py_ssize_t collect(int generation)
{
int i;
PyGC_Head *young; /* 即將查找的一代 */
PyGC_Head *old; /* 下一代 */
PyGC_Head unreachable; /* 無異樣不能到達對象的鏈表 */
PyGC_Head finalizers;
/* 更新計數器 */
if (generation+1 < NUM_GENERATIONS) generations[generation+1].count += 1;
for (i = 0; i <= generation; i++)
generations[i].count = 0;
/* 合並指定的代及其以下的代的鏈表 */
for (i = 0; i < generation; i++) {
gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
}
/* 給old變量賦值 */
young = GEN_HEAD(generation);
if (generation < NUM_GENERATIONS-1)
old = GEN_HEAD(generation+1);
else
old = young;
update_refs(young); /*把引用計數器復制到用於循環引用垃圾回收的頭里 */
subtract_refs(young); /* 刪除實際的引用 */
/* 將計數器值為0的對象移動到不可能到達對象的鏈表 */ gc_list_init(&unreachable);
move_unreachable(young, &unreachable);
/* 將從循環引用垃圾回收中幸存的對象移動到下一代 */
if (young != old)
gc_list_merge(young, old);
/* 移出不可能到達對象的鏈表內有終結器的對象 */ gc_list_init(&finalizers);
move_finalizers(&unreachable, &finalizers); move_finalizer_reachable(&finalizers);
/* 釋放循環引用的對象群 */
delete_garbage(&unreachable, old);
/* 將finalizers鏈表注冊為“不能釋放的垃圾” */ (void)handle_finalizers(&finalizers, old);
}
- 首先將一個老年代的技術局執行增量操作,將制定的代的計數器設置為0。之后所指定的代及其以下代的鏈表合並到自己所屬的代。
- 然后把引用計數器復制到用於循環引用垃圾回收的頭里。從這個計數器刪除實際的引用。循環引用的對象的計數器值會變為0。
- 之后把從GC中幸存下來的對象聯通鏈表一起合並到下一代。讓其晉升。
- 由於某些原因,程序無法釋放有終結器的循環引用對象,所以要將其移除。
循環引用中的終結器
循環引用垃圾回收把帶有終結器的對象排除在處理范圍之外。這是為什么?
當然是因為太復雜了。哈哈
舉個栗子假設兩個對象是循環引用關系,如果他們都有自己的終結器那么先調用那個好?
在第一個對象最終化后,第二個對象也最終化。那么或許在最終化的過程中又用到了第一個對象。也就是說我們絕對不能先最終化第一個對象。
所以在循環引用的垃圾回收中,有終結器的循環引用垃圾對象是排除在GC的對像范圍之外的。
但是有終結器的循環引用對象,能夠作為鏈表在Python內進行處理。如果出現有終結器的循環引用垃圾對象,我們就需要利用這項功能,從應用程序的角度去除對象的循環引用。
python關於GC的模塊
gc.set_debug()(可以查看垃圾回收的信息,進而優化程序)
Python采用引用計數法,所以回收會比較快。但是在面臨循環引用的問題時候,可能要多費一些時間。
在這種情況下,我們可以使用gc模塊的set_debug()來查找原因,進而進行優化程序。
import gc
gc.set_debug(gc.DEBUG_STATS)
gc.collect()
# gc: collecting generation 2...
# gc: objects in each generation: 10 0 13607
# gc: done, 0.0087s elapsed.
一旦用set_debug()設定了gc.DEBUG_STATS標志,那么每次進行循環引用垃圾回收,就會輸出一下信息。
1. GC 對象的代
2. 各代內對象的數量
3. 循環引用垃圾回收所花費的時間
當然除了DEBUG_STATS以外,還可以設置各種標志,關於這些標志可以查看源碼或者官方文檔。
gc.collect()
經過第一步的優化后,如果還是不行,就要用到gc.collect()。
使用gc.collect()就能在應用程序運行的過程中,任意時刻執行循環引用垃圾回收了。
也就是說,我們人為的選擇最合適的時間去進行循環引用的GC。
gc.disable()
一旦調用 gc.disable(),循環引用垃圾回收就停止運作了。也就是說,循環引用的垃圾對象群一直不會得到釋放。 然而從應用程序整體的角度來看,如果循環引用的對象的大小可以忽視,那么這個方法 也不失為一個好方法。這就需要我們自己來權衡了。