1. 程式人生 > 實用技巧 >python -- 記憶體與垃圾回收原始碼分析

python -- 記憶體與垃圾回收原始碼分析

以前剛學python的時候,經常需要對資料進行迴圈操作,但是又需要保留原始資料,就有了下面的程式碼,此程式碼只是描述,不可當真。

    data_list = [1,2,3,4,5]
    temp_list = data_list
    for data in data_list:
        if data == 2 or data == 6:
            temp_list.append(6)

這個程式碼會一直無限迴圈下去,明明是倆個不同的變數,只有data_list和temp_list是一樣的,之後使用id檢視變數的地址。

    data_list = [1,2,3,4,5]
    temp_list = data_list
    temp_list.append(6)
    print(data_list)

    print(id(data_list))
    print(id(temp_list))

之後輸出的結果就是

[1, 2, 3, 4, 5, 6]
2296507176648
2296507176648

之後我想直接先刪除吧,然後再重新建立一個

    data_list = [1,2,3,4,5]
    temp_list = data_list
    temp_list.append(6)
    print(data_list)

    print(id(data_list))
    print(id(temp_list))

    del data_list
    del temp_list

    second_list = [1,2]
    print(id(second_list))

但是結果地址結果竟然都是一樣的

[1, 2, 3, 4, 5, 6]
2296507176648
2296507176648
2296507176648

下面我們就從原始碼的角度來分析這種現象。
目錄

python記憶體管理機制

為了能夠了解記憶體管理,我們最好是從原始碼來看並且分析,因為大學時期學過C語言,因此勉強能看懂,此次我們看的是python3.7.6的原始碼,不過我感覺每版的原始碼應該沒有太大的變化,畢竟基礎原理和語言特性是不會改變的。開啟include中的listObject.h,

typedef struct {
    // object裡面的內容
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    // 頭元素指標
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    // 當前可容納的元素大小
    Py_ssize_t allocated;
} PyListObject;

頭元素指標ob_item就是首地址,這個很好理解,其他語言中也有這種類似的,就是陣列名就是首地址,下面allocated就是list中可容納的元素大小,其實就是list申請了多少記憶體,從註釋中可以看出ob_size是當前元素個數大小,這裡的意思就是列表需要頻繁的插入和刪除,那麼頻繁的申請和釋放記憶體是不明智的,那麼就先申請一大塊記憶體,這個一大塊就是allocated,已經使用的大小就是ob_size,那最上面那個PyObject_VAR_HEAD是什麼呢?我們都知道所有的物件都會繼承object這個物件,我們開啟include中的object.h,在這個檔案的開頭定義中有這麼一行程式碼,

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   PyObject ob_base;

#define PyObject_HEAD_INIT(type)        \
    { _PyObject_EXTRA_INIT              \
    1, type },

#define PyVarObject_HEAD_INIT(type, size)       \
    { PyObject_HEAD_INIT(type) size },

/* PyObject_VAR_HEAD defines the initial segment of all variable-size
 * container objects.  These end with a declaration of an array with 1
 * element, but enough space is malloc'ed so that the array actually
 * has room for ob_size elements.  Note that ob_size is an element count,
 * not necessarily a byte count.
 */
#define PyObject_VAR_HEAD      PyVarObject ob_base;
#define Py_INVALID_SIZE (Py_ssize_t)-1

我們發現PyObject_VAR_HEAD其實就是PyVarObject這個物件,並且也可以發現PyObject_HEAD 就是PyObject,下面就是這倆個結構體的定義

// 只有float是用的它
typedef struct _object {
	// 雙向連結串列
    _PyObject_HEAD_EXTRA

    // 引用計數器
    Py_ssize_t ob_refcnt;

    // 物件型別
    struct _typeobject *ob_type;
} PyObject;

// list,dict,set,tuple,int等
typedef struct {
    // 例項
    PyObject ob_base;
    
    // 容器內的元素個數,比如列表,字典這種
    Py_ssize_t ob_size; /* Number of items in variable part */

} PyVarObject;

從上面的註釋就可以看出它們之間的關係,一個最基本的物件最起碼有雙向連結串列(用於管理python中建立的物件)、引用計數器和物件型別,雙向連結串列和引用計數器主要是為了管理物件和垃圾回收機制的,像list這種PyVarObject會有ob_size,也就是元素個數,但是如果檢視longObject這種資料型別(python3中沒有long型別,只有int,而int是用C中的long實現的)我們發現也是PyVarObject,但是它並不像列表需要放很多元素啊。這個主要還是與它內部實現相關,我們都知道python的整數是可以無限大小的,這裡的無限大小就是很多個digit堆疊起來的,自然也是需要計數的。

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

再看PyObject中,它裡面還存放了資料型別,之後我們需要看看初始化是什麼樣的,list分析的話比較難,涉及到垃圾回收機制,我們先看簡單的,之後分析完垃圾回收機制再來看list的原始碼。

floatObject的操作

我們先看一個簡單的floatObject,因為它是最簡單的PyObject,其他的都是PyVarObject。從Objects找出floatObject.c,

PyObject *
PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        // 先從單項鍊表中拿出來一個。這個單項鍊表就是儲存那些引用計數為0的開闢好的空間,這也是一種快取機制
        // int和字串沒有這種快取機制
        free_list = (PyFloatObject *) Py_TYPE(op);
        // free_list裡面最多有100個
        numfree--;
    } else {
        // 開闢記憶體,深究下面有點複雜
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    // 在開闢好的記憶體中進行初始化
    /* - PyObject_Init(op, typeobj) and PyObject_InitVar(op, typeobj, n) don't
   allocate memory.  Instead of a 'type' parameter, they take a pointer to a
   new object (allocated by an arbitrary allocator), and initialize its object
   header fields.*/
    (void)PyObject_INIT(op, &PyFloat_Type);
    // 將值賦值到開闢的記憶體中
    op->ob_fval = fval;
    // 返回建立物件的記憶體地址的指標
    return (PyObject *) op;
}

從上面的註釋可以看出,如果是剛開始賦值的話,會先開闢記憶體,然後在開闢好的記憶體中進行初始化,我們從objimpl.h中找到這個初始化的方法,其中一個是對物件型別進行賦值,之後的操作都是在_Py_NewReference中的。

#define PyObject_INIT(op, typeobj) \
    ( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
#define PyObject_INIT_VAR(op, typeobj, size) \
    ( Py_SIZE(op) = (size), PyObject_INIT((op), (typeobj)) )

開啟實現檔案object.c,找到_Py_NewReference方法

void
_Py_NewReference(PyObject *op)
{
    _Py_INC_REFTOTAL;
    // 引用計數器為1
    op->ob_refcnt = 1;
    // 新增到雙向連結串列中
    _Py_AddToAllObjects(op, 1);
    _Py_INC_TPALLOCS(op);
}

這個方法做的就是引用計數器為1,之後就是_Py_AddToAllObjects方法,找出這個方法

/* Head of circular doubly-linked list of all objects.  These are linked
 * together via the _ob_prev and _ob_next members of a PyObject, which
 * exist only in a Py_TRACE_REFS build.
 */
static PyObject refchain = {&refchain, &refchain};

/* Insert op at the front of the list of all objects.  If force is true,
 * op is added even if _ob_prev and _ob_next are non-NULL already.  If
 * force is false amd _ob_prev or _ob_next are non-NULL, do nothing.
 * force should be true if and only if op points to freshly allocated,
 * uninitialized memory, or you've unlinked op from the list and are
 * relinking it into the front.
 * Note that objects are normally added to the list via _Py_NewReference,
 * which is called by PyObject_Init.  Not all objects are initialized that
 * way, though; exceptions include statically allocated type objects, and
 * statically allocated singletons (like Py_True and Py_None).
 */
void
_Py_AddToAllObjects(PyObject *op, int force)
{
#ifdef  Py_DEBUG
    if (!force) {
        /* If it's initialized memory, op must be in or out of
         * the list unambiguously.
         */
        assert((op->_ob_prev == NULL) == (op->_ob_next == NULL));
    }
#endif
    // 新增
    if (force || op->_ob_prev == NULL) {
        op->_ob_next = refchain._ob_next;
        op->_ob_prev = &refchain;
        refchain._ob_next->_ob_prev = op;
        refchain._ob_next = op;
    }
}

可以看出refchain是個雙向連結串列中,整個過程就是新增到物件雙向連結串列中。我們來整理一下整個floatObject的初始化過程,如果a = 8.9,那麼它會先開闢記憶體,之後進行初始化,就是型別賦值,引用加1,加入雙向連結串列中,之後將值賦值到開闢的記憶體中,之後返回到a中,a其實就是一個地址的引用而已,那麼我們知道在python中變數本質就是對一塊記憶體資料區域的引用,而不是記憶體中一塊儲存資料的區域。這裡的變數名是沒有型別的,型別是屬於物件的。變數引用什麼型別的物件,物件就是什麼型別的,那麼list也是如此,如果b = a,會執行的就是下面的程式碼

#define Py_INCREF(op) (                         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
    ((PyObject *)(op))->ob_refcnt++)

上面其實就是將引用計數+1而已,那麼temp_list = data_list,其實就是引用計數器再加1,其記憶體空間沒有任何的改變,所以上面的temp_list也會變成data_list操作。之後我們將temp_list和data_list都刪除,再建立一個列表的時候,它的記憶體地址還是一樣,為什麼會這樣?我們先從物件的銷燬來看,先是從object.h中找出Py_CLEAR

#define Py_CLEAR(op)                            \
    do {                                        \
        PyObject *_py_tmp = (PyObject *)(op);   \
        if (_py_tmp != NULL) {                  \
            (op) = NULL;                        \
            Py_DECREF(_py_tmp);                 \
        }                                       \
    } while (0)

執行Py_DECREF,從中找出Py_DECREF

//   #define _Py_DEC_REFTOTAL        _Py_RefTotal--
#define Py_DECREF(op)                                   \
    do {                                                \
        PyObject *_py_decref_tmp = (PyObject *)(op);    \
        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
        --(_py_decref_tmp)->ob_refcnt != 0)             \
            _Py_CHECK_REFCNT(_py_decref_tmp)            \
        else                                            \
            // 進行垃圾回收
            _Py_Dealloc(_py_decref_tmp);                \
    } while (0)

這個函式的if條件就是先執行引用-1,之後再檢查引用是否等於0,如果不等於0的話,那麼使用_Py_CHECK_REFCNT來檢查它的引用是否小於0,如果小於0了,那麼需要處理這種錯誤。那麼上面程式碼中第一個del,只是刪除了引用。

#define _Py_CHECK_REFCNT(OP)                                    \
{       if (((PyObject*)OP)->ob_refcnt < 0)                             \
                _Py_NegativeRefcount(__FILE__, __LINE__,        \
                                     (PyObject *)(OP));         \
}

如果引用等於0的話,那麼要對它進行垃圾回收,執行_Py_Dealloc

void
_Py_Dealloc(PyObject *op)
{
    // 找到float型別的tp_dealloc進行記憶體銷燬
    destructor dealloc = Py_TYPE(op)->tp_dealloc;
    // 從雙向連結串列中移除
    _Py_ForgetReference(op);
    // 呼叫float型別的tp_dealloc進行記憶體銷燬
    (*dealloc)(op);
}

那麼從floatobject.c中找出來

// #define PyFloat_MAXFREELIST    100
static void
float_dealloc(PyFloatObject *op)
{
    if (PyFloat_CheckExact(op)) {
        // 檢查緩衝池個數是否大於最大
        if (numfree >= PyFloat_MAXFREELIST)  {
            // 緩衝滿了,直接將物件銷燬
            PyObject_FREE(op);
            return;
        }
        // 緩衝池+1
        numfree++;
        // 並將要銷燬的資料加入到free_list單向連結串列中
        Py_TYPE(op) = (struct _typeobject *)free_list;
        free_list = op;
    }
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
}

如果引用等於0,那麼先檢查緩衝陣列free_list個數是否已經最大,如果還沒有,那麼緩衝池+1,之後加入到free_list中,大家可以翻到上面物件建立的程式碼PyFloat_FromDouble中,我們發現在開闢空間的時候,是有if條件的,滿足的話,就從free_list中拿出來的,這個free_list就是一個緩衝陣列。python刪除物件所有的引用之後,並沒有直接銷燬掉,而是採用了一種緩衝機制,下次初始化相同型別的數,直接從緩衝中取,就不用重新申請記憶體啥的了。所有上面我們全部刪除,之後再重新定義list,就會發現它們都是一樣的記憶體地址,list、float、dict、tuple都是如此,其中tuple有點特殊,它的free_list有20個元素,第一個元素裡面放的都是空元素的tuple,第二個放的都是有1個元素的tuple,以此下去,最後一個就是放19個元素的元組,並且每一個位置可以存放2000個元組,就是free_list中第2個位置可以存放2000個含有一個元素的元組,所以如果你之前建立的是倆個元素的元組,刪除之後,只有再建立倆個元素的元組,這倆個元組的記憶體地址才會一樣。

但是有的物件用的不是free_list,比如字串和int,字串會先將所有ASCII字元都創建出來然後一直放在記憶體中,之後還會採用字串駐留技術,就是記憶體中如果有這個字串,就不用建立,直接使用原來的地址。int是用的小資料池,它先將[-5,257)中所有的數先創建出來,它認為它們是頻繁被使用的,一直儲存在記憶體中,也就是它們的引用計數永遠都是大於0的,我們也可以從程式碼中找到判斷是否小資料池中的。

//  下面一個是5,一個是257,這個就是小整數池
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

static PyObject *
get_small_int(sdigit ival)
{
    PyObject *v;
    assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
    v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
    Py_INCREF(v);
#ifdef COUNT_ALLOCS
    if (ival >= 0)
        quick_int_allocs++;
    else
        quick_neg_int_allocs++;
#endif
    return v;
}

//  核實是否在小整數池中,如果是的,那麼直接取資料[5,257)
#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

python的垃圾回收機制

引用計數器

從上面的分析中可以得知,每個物件在建立的時候都會有一個引用計數器,而這種變數記憶體方式也註定了python的回收機制是以引用計數為主的。

// 只有float是用的它
typedef struct _object {
	// 雙向連結串列
    _PyObject_HEAD_EXTRA

    // 引用計數器
    Py_ssize_t ob_refcnt;

    // 物件型別
    struct _typeobject *ob_type;
} PyObject;

增加與刪除實際上都有引用計數有關,就是將引用計數加1和引用計數減1。刪除的時候還要判斷是否引用計數為0,如果為0,那麼開啟垃圾回收。

//   #define _Py_DEC_REFTOTAL        _Py_RefTotal--
#define Py_DECREF(op)                                   \
    do {                                                \
        PyObject *_py_decref_tmp = (PyObject *)(op);    \
        if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
        --(_py_decref_tmp)->ob_refcnt != 0)             \
            _Py_CHECK_REFCNT(_py_decref_tmp)            \
        else                                            \
            // 進行垃圾回收
            _Py_Dealloc(_py_decref_tmp);                \
    } while (0)

但是如果僅僅使用引用計數器作為判別標準的話,在迴圈引用問題上會出現BUG,因此還需要標記清除和分代回收機制作為輔助。

標記清除

迴圈引用的問題我們可以看一下程式

    a = [1,2,3]
    b = [2,3,4]
    a.append(b)
    b.append(a)

這種就會產生迴圈引用的問題。那麼python的辦法是再建立一個連結串列,專門存放那些可能會出現迴圈引用問題的資料型別,比如list、tuple、dict和set。之後在某種情況下觸發,掃描連結串列中的每個元素,找到那些不可達物件即可,那麼就可以將之引用計數為0。

分為倆個階段:

  1. 標記階段,GC會將所有的活動物件打上標記
  2. 將那些沒有標記的物件(非活動物件)進行回收

問題就來到了,怎麼判斷哪些是活動物件,哪些是不活動物件???利用有向圖,我們從上面知道所有物件之間通過指標連在一起,物件構成這個有向圖的節點,引用關係構成這個有向圖的邊。從根節點出發,沿著有向邊遍歷物件,可達物件就標記為活動物件,不可達物件就是非活動物件。

缺點就是清除非活動物件的時候,必須掃描整個堆記憶體,哪怕只剩下小部分的活動物件也要掃描所有的物件

分代回收

這是一種空間換時間的操作方式,python根據記憶體物件的存活時間劃分為不同的集合,每個集合就是一個代。python將記憶體分為3代,年輕代、中年代、老年代,分別對應著3個連結串列,它們的垃圾收集頻率隨著存活的時間增大而減小。

新建立的物件都會被分配在年輕代,年輕帶的連結串列達到上限後,python的垃圾回收機制就會被觸發,把那些可以回收的物件回收掉,而那些不會回收的物件就會被移到中年代去,以此類推,老年代物件就是存活時間最久的物件,有可能存活於整個系統的生命週期內。所以真實中一共存在四個連結串列。如圖所示

建立列表的時候,要檢查0代數量+1,是否超出了閾值,如果超過了,就需要進行分代處理。從Modules/gcmodule.c中可以找到判斷的程式碼

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);
    if (g == NULL)
        return PyErr_NoMemory();
    g->gc.gc_refs = 0;
    _PyGCHead_SET_REFS(g, GC_UNTRACKED);
    // 0代數量+1
    _PyRuntime.gc.generations[0].count++; /* number of allocated GC objects */

    // 0代超出自己的閾值,就會進行分代處理
    if (_PyRuntime.gc.generations[0].count > _PyRuntime.gc.generations[0].threshold &&
        _PyRuntime.gc.enabled &&
        _PyRuntime.gc.generations[0].threshold &&
        !_PyRuntime.gc.collecting &&
        !PyErr_Occurred()) {
        _PyRuntime.gc.collecting = 1;
        // 回收
        collect_generations();
        _PyRuntime.gc.collecting = 0;
    }
    op = FROM_GC(g);
    return op;
}

我們可以從中看出其中collect_generations()就是具體回收的程式碼,我們找到其中的程式碼發現了

static Py_ssize_t
collect_generations(void)
{
    int i;
    Py_ssize_t n = 0;

    /* Find the oldest generation (highest numbered) where the count
     * exceeds the threshold.  Objects in the that generation and
     * generations younger than it will be collected. */
     // 倒序迴圈三代,這裡就是2代如果達到閾值需要掃描了,那麼之前的1代也需要掃描
    for (i = NUM_GENERATIONS-1; i >= 0; i--) {
        if (_PyRuntime.gc.generations[i].count > _PyRuntime.gc.generations[i].threshold) {
            /* Avoid quadratic performance degradation in number
               of tracked objects. See comments at the beginning
               of this file, and issue #4074.
            */
            if (i == NUM_GENERATIONS - 1
                && _PyRuntime.gc.long_lived_pending < _PyRuntime.gc.long_lived_total / 4)
                continue;
            // 掃描當前代之前所有代,
            n = collect_with_callback(i);
            break;
        }
    }
    return n;
}

其中迴圈三代的時候,就是倒序迴圈的,這是因為如果高階代達到了閾值了,那麼低代都需要一起掃描,其中掃描的程式碼是在collect_with_callback。

/* Perform garbage collection of a generation and invoke
 * progress callbacks.
 */
static Py_ssize_t
collect_with_callback(int generation)
{
    Py_ssize_t result, collected, uncollectable;
    invoke_gc_callback("start", generation, 0, 0);
    result = collect(generation, &collected, &uncollectable, 0);
    invoke_gc_callback("stop", generation, collected, uncollectable);
    return result;
}

其中具體的程式碼應該是在collect,找到collect。

/* This is the main function.  Read this to understand how the
 * collection process works. */
static Py_ssize_t
collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
        int nofail)
{
    int i;
    Py_ssize_t m = 0; /* # objects collected */
    Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
    PyGC_Head *young; /* the generation we are examining */
    PyGC_Head *old; /* next older generation */
    PyGC_Head unreachable; /* non-problematic unreachable trash */
    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
    PyGC_Head *gc;
    _PyTime_t t1 = 0;   /* initialize to prevent a compiler warning */

    struct gc_generation_stats *stats = &_PyRuntime.gc.generation_stats[generation];

    if (_PyRuntime.gc.debug & DEBUG_STATS) {
        PySys_WriteStderr("gc: collecting generation %d...\n",
                          generation);
        PySys_WriteStderr("gc: objects in each generation:");
        for (i = 0; i < NUM_GENERATIONS; i++)
            PySys_FormatStderr(" %zd",
                              gc_list_size(GEN_HEAD(i)));
        PySys_WriteStderr("\ngc: objects in permanent generation: %zd",
                         gc_list_size(&_PyRuntime.gc.permanent_generation.head));
        t1 = _PyTime_GetMonotonicClock();

        PySys_WriteStderr("\n");
    }

    if (PyDTrace_GC_START_ENABLED())
        PyDTrace_GC_START(generation);

    /* update collection and allocation counters */
    // 當前代掃描一次,那麼高階代次數要+1
    if (generation+1 < NUM_GENERATIONS)
        _PyRuntime.gc.generations[generation+1].count += 1;
    // 比當前代低的代,次數會設定為0,因為當前代掃描會帶著年輕代一起掃描的,掃描後年輕代的物件會升到高階代中,年輕代就是0
    for (i = 0; i <= generation; i++)
        _PyRuntime.gc.generations[i].count = 0;
    // 高的+1,低的為0

    /* merge younger generations with one we are currently collecting */
    // 將比自己低的所有代,都放在一個連結串列中
    for (i = 0; i < generation; i++) {
        gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
    }

    /* handy references */
    // 獲取連結串列頭
    young = GEN_HEAD(generation);
    // 獲取比當前代高的代的連結串列頭,比如當前代是1,那麼old就是2代,young就是0代和1代
    if (generation < NUM_GENERATIONS-1)
        old = GEN_HEAD(generation+1);
    else
        old = young;

    /* Using ob_refcnt and gc_refs, calculate which objects in the
     * container set are reachable from outside the set (i.e., have a
     * refcount greater than 0 when all the references within the
     * set are taken into account).
     */
    // 為了在迴圈處理代中資料的時候不更改資料,那麼先拷貝一份所有資料的引用計數到gc_refs,之後對gc_refs進行操作
    // 如果拷貝中的引用計數為0,那麼再處理連結串列中的資料
    update_refs(young);
    // 這個函式就是處理迴圈引用,將迴圈引用的資料的引用計數變成0
    subtract_refs(young);

    /* Leave everything reachable from outside young in young, and move
     * everything else (in young) to unreachable.
     * NOTE:  This used to move the reachable objects into a reachable
     * set instead.  But most things usually turn out to be reachable,
     * so it's more efficient to move the unreachable things.
     */
    // 將連結串列中所有的引用計數器為0的,移動到不可達連結串列中
    // 迴圈處理young中的每個資料,然後看gc_refs是否為0,如果是0就放到不可達連結串列中
    gc_list_init(&unreachable);
    move_unreachable(young, &unreachable);

    /* Move reachable objects to next generation. */
    // 將可達資料放入到下一代中
    if (young != old) {
        // 如果是0,1代,那麼升級到下一代
        if (generation == NUM_GENERATIONS - 2) {
            _PyRuntime.gc.long_lived_pending += gc_list_size(young);
        }
        // 把將young連結串列拼接到old連結串列中
        gc_list_merge(young, old);
    }
    else {
        /* We only untrack dicts in full collections, to avoid quadratic
           dict build-up. See issue #14775. */
        // 如果是2代,那麼更新long_lived_pending和long_lived_total
        untrack_dicts(young);
        _PyRuntime.gc.long_lived_pending = 0;
        _PyRuntime.gc.long_lived_total = gc_list_size(young);
    }

    /* All objects in unreachable are trash, but objects reachable from
     * legacy finalizers (e.g. tp_del) can't safely be deleted.
     */
    // 迴圈所有不可達元素,把具有__del__方法資料放到finalizers,
    gc_list_init(&finalizers);
    move_legacy_finalizers(&unreachable, &finalizers);
    /* finalizers contains the unreachable objects with a legacy finalizer;
     * unreachable objects reachable *from* those are also uncollectable,
     * and we move those into the finalizers list too.
     */
    move_legacy_finalizer_reachable(&finalizers);

    /* Print debugging information. */
    if (_PyRuntime.gc.debug & DEBUG_COLLECTABLE) {
        for (gc = unreachable.gc.gc_next; gc != &unreachable; gc = gc->gc.gc_next) {
            debug_cycle("collectable", FROM_GC(gc));
        }
    }

    /* Clear weakrefs and invoke callbacks as necessary. */
    m += handle_weakrefs(&unreachable, old);

    /* Call tp_finalize on objects which have one. */
    // 處理那些具有del方法的資料
    finalize_garbage(&unreachable);
    
    // 清除垃圾
    if (check_garbage(&unreachable)) {
        revive_garbage(&unreachable);
        gc_list_merge(&unreachable, old);
    }
    else {
        /* Call tp_clear on objects in the unreachable set.  This will cause
         * the reference cycles to be broken.  It may also cause some objects
         * in finalizers to be freed.
         */
        m += gc_list_size(&unreachable);
        delete_garbage(&unreachable, old);
    }

    /* Collect statistics on uncollectable objects found and print
     * debugging information. */
    for (gc = finalizers.gc.gc_next;
         gc != &finalizers;
         gc = gc->gc.gc_next) {
        n++;
        if (_PyRuntime.gc.debug & DEBUG_UNCOLLECTABLE)
            debug_cycle("uncollectable", FROM_GC(gc));
    }
    if (_PyRuntime.gc.debug & DEBUG_STATS) {
        _PyTime_t t2 = _PyTime_GetMonotonicClock();

        if (m == 0 && n == 0)
            PySys_WriteStderr("gc: done");
        else
            PySys_FormatStderr(
                "gc: done, %zd unreachable, %zd uncollectable",
                n+m, n);
        PySys_WriteStderr(", %.4fs elapsed\n",
                          _PyTime_AsSecondsDouble(t2 - t1));
    }

    /* Append instances in the uncollectable set to a Python
     * reachable list of garbage.  The programmer has to deal with
     * this if they insist on creating this type of structure.
     */
    handle_legacy_finalizers(&finalizers, old);

    /* Clear free list only during the collection of the highest
     * generation */
    if (generation == NUM_GENERATIONS-1) {
        clear_freelists();
    }

    if (PyErr_Occurred()) {
        if (nofail) {
            PyErr_Clear();
        }
        else {
            if (gc_str == NULL)
                gc_str = PyUnicode_FromString("garbage collection");
            PyErr_WriteUnraisable(gc_str);
            Py_FatalError("unexpected exception during garbage collection");
        }
    }

    /* Update stats */
    if (n_collected)
        *n_collected = m;
    if (n_uncollectable)
        *n_uncollectable = n;
    stats->collections++;
    stats->collected += m;
    stats->uncollectable += n;

    if (PyDTrace_GC_DONE_ENABLED())
        PyDTrace_GC_DONE(n+m);

    return n+m;
}

可以看出其中的程式碼非常的複雜,其實先是進行一些處理,就是當前代進行掃描的話,那麼高的代的掃描次數+1,低代的次數設定為0,之後將低代和當前代放入到young連結串列中,高代放入到old中,然後掃描young連結串列,先拷貝出所有物件的引用計數到gc_refs,之後迴圈遍歷連結串列找出迴圈引用物件,將迴圈引用物件設定為0,並將這些物件放入到不可達連結串列中,將那些可達物件放入到下一代中,之後都是刪除迴圈引用物件,注意有del的物件要特殊處理。

listObject中的操作

下面我們來看list的初始化,找到listObject.c程式碼,

PyObject *
PyList_New(Py_ssize_t size)
{
    // 列表物件
    PyListObject *op;
#ifdef SHOW_ALLOC_COUNT
    static int initialized = 0;
    if (!initialized) {
        Py_AtExit(show_alloc);
        initialized = 1;
    }
#endif
    
    // 如果列表大小0,直接返回
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
   
    if (numfree) {
        // 如果緩衝中有物件,直接拿一個
        numfree--;
        op = free_list[numfree];
        _Py_NewReference((PyObject *)op);
#ifdef SHOW_ALLOC_COUNT
        count_reuse++;
#endif
    } else {
        // 如果沒有,開闢記憶體,他會檢查0代連結串列是不是達到700了
        op = PyObject_GC_New(PyListObject, &PyList_Type);
        if (op == NULL)
            return NULL;
#ifdef SHOW_ALLOC_COUNT
        count_alloc++;
#endif
    }

    // 為物件維護元素列表申請空間
    if (size <= 0)
        op->ob_item = NULL;
    else {
        op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
        if (op->ob_item == NULL) {
            Py_DECREF(op);
            return PyErr_NoMemory();
        }
    }
    Py_SIZE(op) = size;
    op->allocated = size;
    // 把物件加入到分代回收的0代連結串列中
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

從上面可以看出如果緩衝區有的話,是直接拿的,但是如果緩衝區沒有,自己開闢記憶體空間的話,是呼叫了PyObject_GC_New,GC一般都是指垃圾回收機制,難道開闢空間與之有關,檢視Modules/gcmodule.c檔案,找到這個方法。

PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    // 建立物件
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        // 初始化物件並且放入到refchain連結串列中
        op = PyObject_INIT(op, tp);
    return op;
}

可以看出_PyObject_GC_Malloc來建立的物件,那麼找到它,發現它就是上面的那個垃圾回收機制的開始函式_PyObject_GC_Alloc。

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    return _PyObject_GC_Alloc(0, basicsize);
}

那麼list在建立物件的時候,先檢視緩衝區,如果緩衝區沒有的話,開闢記憶體空間,在開闢記憶體空間的時候,需要檢查0代是否已滿,滿的話,需要進行分代處理,就是每一代都需要進行標記清除,然後開闢好空間之後,為物件維護元素列表申請空間,再往這個空間裡賦值,最後將這個物件加入到0代中。

總結

這篇文章主要就是總結一下python語言中的記憶體儲存機制和垃圾回收機制,這倆個其實是深入學習python的第一步,因為從那時開始,我就開始思考python語言的背後機制,也開始學會了讀一些原始碼,之前也看過雨痕的python學習筆記,不過他的筆記是python2的,其中有一些機制已經發生了改變,不過確實是那個筆記讓我發現了新世界。