Python對象的循環引用問題

Python對象循環引用

咱們來介紹一下 Python 是採用何種途徑解決循環引用問題的。python

循環引用垃圾回收算法

上圖中,表示的是對象之間的引用關係,從自對象指向他對象的引用用黑色箭頭表示。每一個對象裏都有計數器。而圖中右側部分能夠很清晰的看到是循環引用的垃圾對象。算法

上圖,將每一個對象的引用計數器複製到本身的另外一個存儲空間中。函數

上圖其實和圖二(圖片左上角)沒什麼區別,只不過更清晰了。由於對象原本就是由對象鏈表鏈接的。只不過是把對象鏈表畫了出來優化

上圖中,將新複製的計數器都進行了減量的操做。先不要管爲何,繼續往下看。debug

可是能夠看到,由根直接引用的對象中,新複製的計數器並無減量。3d

以上操做執行完畢後,再把對象分爲可能到達的對象鏈表不可能到達的對象鏈表code

以後將具有以下條件的對象鏈接到「可能到達對象的鏈表」htm

  1. 通過 (4) 的減量操做後計數器值大於等於 1。
  2. 有從活動對象的引用。

再將具有以下條件的對象鏈接到「不可能到達對象的鏈表」對象

  1. 通過 (4) 的減量操做後計數器值爲 0
  2. 沒有從活動對象的引用

如今上圖顯示的就是垃圾對象鏈表和活動對象的鏈表了。接下來的步驟就是釋放不可能到達的對象,再把可能到達的對象鏈接到對象鏈表。

這樣,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()裏。用於判斷對象是否是正在追蹤,若是是就結束追蹤。
  • _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模塊的文檔

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(),循環引用垃圾回收就中止運做了。也就是說,循環引用的垃圾對象羣一直不會獲得釋放。 然而從應用程序總體的角度來看,若是循環引用的對象的大小能夠忽視,那麼這個方法 也不失爲一個好方法。這就須要咱們本身來權衡了。

相關文章
相關標籤/搜索