目錄html
咱們來介紹一下 Python 是採用何種途徑解決循環引用問題的。python
上圖中,表示的是對象之間的引用關係,從自對象指向他對象的引用用黑色箭頭表示。每一個對象裏都有計數器。而圖中右側部分能夠很清晰的看到是循環引用的垃圾對象。算法
上圖,將每一個對象的引用計數器複製到本身的另外一個存儲空間中。函數
上圖其實和圖二(圖片左上角)沒什麼區別,只不過更清晰了。由於對象原本就是由對象鏈表鏈接的。只不過是把對象鏈表畫了出來。優化
上圖中,將新複製的計數器都進行了減量的操做。先不要管爲何,繼續往下看。debug
可是能夠看到,由根直接引用的對象中,新複製的計數器並無減量。3d
以上操做執行完畢後,再把對象分爲可能到達的對象鏈表和不可能到達的對象鏈表。code
以後將具有以下條件的對象鏈接到「可能到達對象的鏈表」。htm
再將具有以下條件的對象鏈接到「不可能到達對象的鏈表」。對象
如今上圖顯示的就是垃圾對象鏈表和活動對象的鏈表了。接下來的步驟就是釋放不可能到達的對象,再把可能到達的對象鏈接到對象鏈表。
這樣,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; }
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)
這樣一來就把全部容器對象都鏈接到了做爲容器對象鏈表的雙向鏈表中。循環引用垃圾回收就是用這個容器對象鏈表來釋放循環引用對象的。
經過引用計數法釋放容器對象以前,要把容器對象從容器對象鏈表中取出。由於呢沒有必要去追蹤已經釋放了的對象,因此這麼作是理所應當的。下面以字典對象爲例釋放字典的函數。
大多數狀況下是經過引用計數法的減量操做來釋放容器對象的,由於循環引用垃圾回收釋放的知識具備循環引用關係的對象羣,因此數量並無那麼多。
容器對象鏈表分爲三代。循環引用垃圾回收事實上是分帶垃圾回收。
系統經過下面的結構體來管理各代容器對象鏈表。
struct gc_generation { PyGC_Head head; int threshold; /* 開始GC的閾值 */ int 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; }
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); }
循環引用垃圾回收把帶有終結器的對象排除在處理範圍以外。這是爲何?
固然是由於太複雜了。哈哈
舉個栗子假設兩個對象是循環引用關係,若是他們都有本身的終結器那麼先調用那個好?
在第一個對象最終化後,第二個對象也最終化。那麼或許在最終化的過程當中又用到了第一個對象。也就是說咱們絕對不能先最終化第一個對象。
因此在循環引用的垃圾回收中,有終結器的循環引用垃圾對象是排除在GC的對像範圍以外的。
可是有終結器的循環引用對象,可以做爲鏈表在Python內進行處理。若是出現有終結器的循環引用垃圾對象,咱們就須要利用這項功能,從應用程序的角度去除對象的循環引用。
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。
一旦調用 gc.disable(),循環引用垃圾回收就中止運做了。也就是說,循環引用的垃圾對象羣一直不會獲得釋放。 然而從應用程序總體的角度來看,若是循環引用的對象的大小能夠忽視,那麼這個方法 也不失爲一個好方法。這就須要咱們本身來權衡了。