《python解釋器源碼剖析》第17章--python的內存管理與垃圾回收

17.0 序

內存管理,對於python這樣的動態語言是相當重要的一部分,它在很大程度上決定了python的執行效率,由於在python的運行中會建立和銷燬大量的對象,這些都設計內存的管理。同理python還提供了了內存的垃圾回收(GC,garbage collection),將開發者從繁瑣的手動維護內存的工做中解放出來。這一章咱們就來分析python的GC是如何實現的。java

17.1 內存管理架構

在python中內存管理機制是分層次的,咱們能夠當作有四層,0 1 2 3。在最底層,也就是第0層是由操做系統提供的內存管理接口,好比C提供了malloc和free接口,這一層是由操做系統實現而且管理的,python不能干涉這一行爲。從這一層往上,剩餘的三層則都是由python實現並維護的。python

第一層是python基於第0層操做系統管理接口包裝而成的,這一層並無在第0層上加入太多的動做,其目的僅僅是爲python提供一層統一的raw memory的管理接口。這麼作的緣由就是雖然不一樣的操做系統都提供了ANSI C標準所定義的內存管理接口,可是對於某些特殊狀況不一樣操做系統有不一樣的行爲。好比調用malloc(0),有的操做系統會返回NULL,表示申請失敗,可是有的操做系統則會返回一個貌似正常的指針, 可是這個指針指向的內存並非有效的。爲了最普遍的可移植性,python必須保證相同的語義必定表明着相同的運行時行爲,爲了處理這些與平臺相關的內存分配行爲,python必需要在C的內存分配接口之上再提供一層包裝。linux

在python中,第一層的實現就是一組以PyMem_爲前綴的函數族,下面來看一下。程序員

//include.h
PyAPI_FUNC(void *) PyMem_Malloc(size_t size);
PyAPI_FUNC(void *) PyMem_Realloc(void *ptr, size_t new_size);
PyAPI_FUNC(void) PyMem_Free(void *ptr);

//obmalloc.c
void *
PyMem_Malloc(size_t size)
{
    /* see PyMem_RawMalloc() */
    if (size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyMem.malloc(_PyMem.ctx, size);
}

void *
PyMem_Realloc(void *ptr, size_t new_size)
{
    /* see PyMem_RawMalloc() */
    if (new_size > (size_t)PY_SSIZE_T_MAX)
        return NULL;
    return _PyMem.realloc(_PyMem.ctx, ptr, new_size);
}

void
PyMem_Free(void *ptr)
{
    _PyMem.free(_PyMem.ctx, ptr);
}

咱們看到在第一層,python提供了相似於相似於C中malloc、realloc、free的語義。而且咱們發現,好比PyMem_Malloc,若是申請的內存大小超過了PY_SSIZE_T_MAX直接返回NULL,而且調用了_PyMem.malloc,這個C中的malloc幾乎沒啥區別,可是會對特殊值進行一些處理。到目前爲止,僅僅是分配了raw memory而已。其實在第一層,python還提供了面向對象中類型的內存分配器。算法

//pymem.h
#define PyMem_New(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :      \
        ( (type *) PyMem_Malloc((n) * sizeof(type)) ) )
#define PyMem_NEW(type, n) \
  ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :      \
        ( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )
#define PyMem_Resize(p, type, n) \
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :        \
        (type *) PyMem_Realloc((p), (n) * sizeof(type)) )
#define PyMem_RESIZE(p, type, n) \
  ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL :        \
        (type *) PyMem_REALLOC((p), (n) * sizeof(type)) )
#define PyMem_Del               PyMem_Free
#define PyMem_DEL               PyMem_FREE

很明顯,在PyMem_Malloc中須要程序員自行提供所申請的空間大小。然而在PyMem_New中,只須要提供類型和數量,python會自動偵測其所需的內存空間大小。c#

第一層所提供的內存管理接口的功能是很是有限的,若是建立一個PyLongObject對象,還須要作不少額外的工做,好比設置對象的類型參數、初始化對象的引用計數值等等。所以爲了簡化python自身的開發,python在比第一層更高的抽象層次上提供了第二層內存管理接口。在這一層,是一組以PyObject_爲前綴的函數族,主要提供了建立python對象的接口。這一套函數族又被換作Pymalloc機制。所以在第二層的內存管理機制上,python對於一些內建對象構建了更高抽象層次的內存管理策略。而對於第三層的內存管理策略,主要就是對象的緩存機制。所以:數組

第0層:操做系統負責管理內存,python無權干預緩存

第1層:僅僅對c中原生的malloc進行了簡單包裝安全

第2層:真正在python中發揮巨大做用,而且也是GC的藏身之處架構

第3層:緩衝池,好比小整數對象池等等。

下面咱們就來對第二層內存管理機制進行剖析。

17.2 小塊空間的內存池

在python中,不少時候申請的內存都是小塊的內存,這些小塊的內存在申請後很快又被釋放,而且這些內存的申請並非爲了建立對象,因此並無對象一級的內存池機制。這就意味着python在運行期間須要大量的執行malloc和free操做,致使操做系統在用戶態和內核態之間進行切換,這將嚴重影響python的效率。因此爲了提升執行效率,python引入了一個內存池機制,用於管理對小塊內存的申請和釋放,這就是以前說的Pymalloc機制,而且提供了pymalloc_allocpymalloc_reallocpymalloc_free三個接口。

整個小塊內存的內存池能夠視爲一個層次結構,在這個層次結構中一共分爲4層,從下至上分別是:block、pool、arena和內存池。而且block(霧)、pool、arena都是python代碼中能夠找到的實體,而最頂層的內存池只是一個概念上的東西,表示python對整個小塊內存分配和釋放行爲的內存管理機制。

17.2.1 block

在最底層,block是一個肯定大小的內存塊。而python中,有不少種block,不一樣種類的block都有不一樣的內存大小,這個內存大小的值被稱之爲size class。爲了在當前主流的32位平臺和64位平臺都能得到最佳性能,全部的block的長度都是8字節對齊的。

//obmalloc.c
#define ALIGNMENT               8               /* must be 2^N */
#define ALIGNMENT_SHIFT         3

同時,python爲block的大小設定了一個上限,當申請的內存大小小於這個上限時,python能夠使用不一樣種類的block知足對內存的需求;當申請的內存大小超過了這個上限,python就會將對內存的請求轉交給第一層的內存管理機制,即PyMem函數族來處理。這個上限值在python中被設置爲512,若是超過了這個值仍是要通過操做系統臨時申請的。

//obmalloc.c
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

根據SMALL_REQUEST_THRESHOLDALIGNMENT的限定,實際上咱們能夠由此獲得不一樣種類的block和size class。block是以8字節對齊,那麼每個塊的大小都是8的整倍數,最大不超過512

* Request in bytes     Size of allocated block      Size class idx
 * ----------------------------------------------------------------
 *        1-8                     8                       0
 *        9-16                   16                       1
 *       17-24                   24                       2
 *       25-32                   32                       3
 *       33-40                   40                       4
 *       41-48                   48                       5
 *       49-56                   56                       6
 *       57-64                   64                       7
 *       65-72                   72                       8
 *        ...                   ...                     ...
 *      497-504                 504                      62
 *      505-512                 512                      63

所以當咱們申請一個44字節的內存時,PyObject_Malloc會從內存池中劃分一個48字節的block給咱們。

另外在python中,block只是一個概念,在python源碼中沒有與之對應的實體存在。以前咱們說對象,對象在源碼中有對應的PyObject,列表在源碼中則有對應的PyListObject,可是這裏的block僅僅是概念上的東西,咱們知道它是具備必定大小的內存,可是它並不與python源碼裏面的某個東西對應。可是,python提供了一個管理block的東西,也就是咱們下面要分析的pool。

17.2.2 pool

一組block的集合成爲一個pool,換句話說,一個pool管理着一堆具備固定大小的內存塊(block)。事實上,pool管理者一大塊內存,它有必定的策略,將這塊大的內存劃分爲多個小的內存塊。在python中,一個pool的大小一般是爲一個系統內存頁,也就是4kb。

//obmalloc.c
#define SYSTEM_PAGE_SIZE        (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK   (SYSTEM_PAGE_SIZE - 1)
#define POOL_SIZE               SYSTEM_PAGE_SIZE        /* must be 2^N */
#define POOL_SIZE_MASK          SYSTEM_PAGE_SIZE_MASK

雖然python沒有爲block提供對應的結構,可是提供了和pool相關的結構,咱們來看看

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 固然pool裏面的block數量    */
    block *freeblock;                   /* 一個鏈表,指向下一個可用的block   */
    struct pool_header *nextpool;       /* 指向下一個pool  */
    struct pool_header *prevpool;       /* 指向上一個pool       ""        */
    uint arenaindex;                    /* 在area裏面的索引 */
    uint szidx;                         /* block的大小(固定值?後面說)     */
    uint nextoffset;                    /* 下一個可用block的內存偏移量         */
    uint maxnextoffset;                 /* 最後一個block距離開始位置的距離     */
};

typedef struct pool_header *poolp;

咱們剛纔說了一個pool的大小在python中是4KB,可是從當前的這個pool的結構體來看,用鼻子想也知道吃不完4KB的內存。因此呀,這個結構體叫作pool_header,它僅僅一個pool的頭部,除去這個pool_header,還剩下的內存纔是維護的全部block的集合所佔的內存。

咱們注意到,pool_header裏面有一個szidx,這就意味着pool裏面管理的內存塊大小都是同樣的。也就是說,一個pool可能管理了20個32字節的block、也可能管理了20個64字節的block,可是不會出現管理了10個32字節的block加上10個64字節的block存在。每個pool都和一個size聯繫在一塊兒,更確切的說都和一個size class index聯繫在一塊兒,表示pool裏面存儲的block都是多少字節的。這就是裏面的域szidx存在的意義。

假設咱們手裏有一塊4kb的內存,來看看python是如何將這塊內存改造爲一個管理32字節block的pool,並從中取出第一塊pool的。

//obmalloc.c
#define POOL_OVERHEAD   _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)


static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    ...	
    //pool指向了一塊4kb的內存
    init_pool:
        pool->ref.count = 1;
        ...	    	
        //設置pool的size class index    
        pool->szidx = size;
        //將size class index轉換成size,好比:0->8, 1->16, 63->512
        size = INDEX2SIZE(size);
        //跳過用於pool_header的內存,並進行對齊
        bp = (block *)pool + POOL_OVERHEAD;
        //等價於pool->nextoffset = POOL_OVERHEAD+size+size
        pool->nextoffset = POOL_OVERHEAD + (size << 1);
        pool->maxnextoffset = POOL_SIZE - size;
        pool->freeblock = bp + size;
        *(block **)(pool->freeblock) = NULL;
        goto success;
    ...
success:
    UNLOCK();
    assert(bp != NULL);
    *ptr_p = (void *)bp;
    return 1;
}

最後的(void *)bp;就是指向從pool中取出的第一塊block的指針。也就是說pool中第一塊block已經被分配了,因此在ref.count中記錄了當前已經被分配的block的數量,這時爲1,特別須要注意的是,bp返回的其實是一個地址,這個地址以後有將近4kb的內存實際上都是可用的,可是能夠確定申請內存的函數只會使用[bp, bp+size]這個區間的內存,這是由size class index能夠保證的。改形成pool以後的4kb內存如圖所示:

實線箭頭是指針,可是虛線箭頭則是偏移位置的形象表示。在nextoffset,maxnextoffset中存儲的是相對於pool頭部的偏移位置。

在瞭解初始化以後的pool的樣子以後,能夠來看看python在申請block時,pool_header中的各個域是怎麼變更的。假設咱們從如今開始連續申請5塊28字節內存,因爲28字節對應的size class index爲3,因此實際上會申請5塊32字節的內存。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    if (pool != pool->nextpool) {
        /*
         * There is a used pool for this size class.
         * Pick up the head block of its free list.
         */
        //首先pool中block數自增1
        ++pool->ref.count;
        //這裏的freeblock指向的是下一個可用的block的起始地址
        bp = pool->freeblock;
        assert(bp != NULL);
        if ((pool->freeblock = *(block **)bp) != NULL) {
            goto success;
        }
		
        //所以當再次申請32字節block時,只須要返回freeblock指向的地址就能夠了。那麼很顯然,freeblock須要前進,指向下一個可用的block,這個時候nextoffset就現身了
        if (pool->nextoffset <= pool->maxnextoffset) {
            //當nextoffset小於等於maxoffset時候
            //freeblock等於當前block的地址 + nextoffset(下一個可用block的內存偏移量)
            //因此freeblock正好指向了下一個可用block的地址
            pool->freeblock = (block*)pool +
                              pool->nextoffset;
            //同理,nextoffset也要向前移動一個block的距離
            pool->nextoffset += INDEX2SIZE(size);
            //依次反覆,便可對全部的block進行遍歷。而maxnextoffset指明瞭該pool中最後一個可用的block距離pool開始位置的偏移
            //當pool->nextoffset > pool->maxnextoffset就意味着遍歷完pool中的全部block了
            //再次獲取顯然就是NULL了
            *(block **)(pool->freeblock) = NULL;
            goto success;
        }
}

因此,申請、前進、申請、前進,一直重複着相同的動做,整個過程很是天然,也容易理解。可是咱們發現,因爲不管多少個block,這些block必須都是具備相同大小,致使一個pool中只能知足POOL_SIZE / size次對block的申請,這就讓人不舒服。舉個栗子,如今咱們已經進行了5次連續32字節的內存分配,能夠想象,pool中5個連續的block都被分配出去了。過了一段時間,程序釋放了其中的第2塊和第4塊block,那麼下一次再分配32字節的內存的時候,pool提交的應該是第2塊,仍是第6塊呢?顯然爲了pool的使用效率,最好分配自由的第二塊block。所以能夠想象,一旦python運轉起來,內存的釋放動做將致使pool中出現大量的離散的自由block,python爲了知道哪些block是被使用以後再次被釋放的,必須創建一種機制,將這些離散自由的block組合起來,再次使用。這個機制就是全部的自由block鏈表,這個鏈表的關鍵就在pool_header中的那個freeblock身上。

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 固然pool裏面的block數量    */
    block *freeblock;                   /* 一個鏈表,指向下一個可用的block   */
    struct pool_header *nextpool;       /* 指向下一個pool  */
    struct pool_header *prevpool;       /* 指向上一個pool       ""        */
    uint arenaindex;                    /* 在area裏面的索引 */
    uint szidx;                         /* block的大小(固定值?後面說)     */
    uint nextoffset;                    /* 下一個可用block的內存偏移量         */
    uint maxnextoffset;                 /* 最後一個block距離開始位置的距離     */
};

typedef struct pool_header *poolp;

剛纔咱們說了,當pool初始化完後以後,freeblock指向了一個有效的地址,也就是下一個能夠分配出去的block的地址。然而奇特的是,當python設置了freeblock時,還設置了*freeblock。這個動做看似詭異,然而咱們立刻就能看到設置*freeblock的動做正是創建離散自由block鏈表的關鍵所在。目前咱們看到的freeblock只是在機械地前進前進,由於它在等待一個特殊的時刻,在這個特殊的時刻,你會發現freeblock開始成爲一個甦醒的精靈,在這4kb的內存上開始靈活地舞動。這個特殊的時刻就是一個block被釋放的時刻。

//obmalloc.c

//基於地址P得到離P最近的pool的邊界地址
#define POOL_ADDR(P) ((poolp)_Py_ALIGN_DOWN((P), POOL_SIZE))

static int
pymalloc_free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;

    pool = POOL_ADDR(p);
    //若是p不在pool裏面,直接返回0
    if (!address_in_range(p, pool)) {
        return 0;
    }
    LOCK();
	
    //釋放,那麼ref.count就是勢必大於0
    assert(pool->ref.count > 0);            /* else it was empty */
    *(block **)p = lastfree = pool->freeblock;
    pool->freeblock = (block *)p;
}

在釋放block時,神祕的freeblock驚鴻一現,覆蓋在freeblock身上的那層面紗就要被揭開了。咱們知道,這是freeblock雖然指向了一個有效的pool裏面的地址,可是*freeblock是爲NULL的。假設這時候python釋放的是block A,那麼A中的第一個字節的值被設置成了當前freeblock的值,而後freeblock的值被更新了,指向了block A的首地址。就是這兩個步驟,一個block被插入到了離散自由的block鏈表中,因此當第2塊和第4塊block都被釋放以後,咱們能夠看到一個初具規模的離散自由block鏈表了。

到了這裏,這條實現方式很是奇特的block鏈表被咱們挖掘出來了,從freeblock開始,咱們能夠很容易的以freeblock = *freeblock的方式遍歷這條鏈表,而當發現了*freeblock爲NULL時,則代表到達了該鏈表(可用自由鏈表)的尾部了,那麼下次就須要申請新的block了。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void *p)
{
        if (pool != pool->nextpool) {
        ++pool->ref.count;
        bp = pool->freeblock;
        assert(bp != NULL);
        //若是這裏的條件不爲真,代表離散自由鏈表中已經不存在可用的block了
        //若是可能,則會繼續分配pool的nextoffset指定的下一塊block
        if ((pool->freeblock = *(block **)bp) != NULL) {
            goto success;
        }

        /*
         * Reached the end of the free list, try to extend it.
         */
        if (pool->nextoffset <= pool->maxnextoffset) {
            ...
        }
}

可是若是連pool->nextoffset <= pool->maxnextoffset這個條件都不成立了呢?pool的大小有限制啊,若是我再想申請block的時候,沒空間了怎麼辦?再來一個pool不就行了,因此多個block能夠組合成一個集合,pool;那麼多個pool也能夠組合起來,就是咱們下面介紹的arena。

17.2.3 arena

在python中,多個pool聚合的結果就是一個arena。上一節提到,pool的大小默認是4kb,一樣每一個arena的大小也有一個默認值。#define ARENA_SIZE (256 << 10) ,顯然這個值默認是256KB,也就是ARENA_SIZE / POOL_SIZE = 64個pool的大小。

//obmalloc.c
struct arena_object {
    uintptr_t address;
    block* pool_address;
    uint nfreepools;
    uint ntotalpools;
    struct pool_header* freepools;
    struct arena_object* nextarena;
    struct arena_object* prevarena;
};

一個概念上的arena在python源碼中就對應arena_object結構體,確切的說,arena_object僅僅是arena的一部分。就像pool_header僅僅是pool的一部分同樣,一個完整的pool包括一個pool_header和透過這個pool_header管理着的block集合;一個完整的arena也包括一個arena_object和透過這個arena_object管理着的pool集合。

"未使用的"的arena和"可用"的arena

在arena_object結構體的定義中,咱們看到了nextarena和prevarena這兩個東西,這彷佛意味着在python中會有一個或多個arena構成的鏈表,這個鏈表的表頭就是arenas。呃,這種猜想實際上只對了一半,實際上,在python中確實會存在多個arena_object構成的集合,可是這個集合不夠成鏈表,而是一個數組。數組的首地址由arenas來維護,這個數組就是python中的通用小塊內存的內存池。另外一方面,nextarea和prevarena也確實是用來鏈接arena_object組成鏈表的,咦,不是已經構成或數組了嗎?爲啥又要來一個鏈表。

咱們曾說arena是用來管理一組pool的集合的,arena_object的做用看上去和pool_header的做用是同樣的。可是實際上,pool_header管理的內存和arena_object管理的內存有一點細微的差異。pool_header管理的內存pool_header自身是一塊連續的內存,可是arena_object與其管理的內存則是分離的:

咋一看,貌似沒啥區別,不過一個是連着的,一個是分開的。可是這後面隱藏了這樣一個事實:當pool_header被申請時,它所管理的內存也必定被申請了;可是當arena_object被申請時,它所管理的pool集合的內存則沒有被申請。換句話說,arena_object和pool集合在某一時刻須要創建聯繫。

當一個arena的arena_object沒有與pool創建聯繫的時候,這時的arena就處於"未使用"狀態;一旦創建了聯繫,這時arena就轉換到了"可用"狀態。對於每一種狀態,都有一個arena鏈表。"未使用"的arena鏈表表頭是unused_arena_objects,多個arena之間經過nextarena鏈接,而且是一個單向的鏈表;而"可用的"arena鏈表表頭是usable_arenas,多個arena之間經過nextarena、prevarena鏈接,是一個雙向鏈表。

申請arena

在運行期間,python使用new_arena來建立一個arena,咱們來看看它是如何被建立的。

//obmalloc.c

//arenas,多個arena組成的數組的首地址
static struct arena_object* arenas = NULL;

//當arena數組中的全部arena的個數
static uint maxarenas = 0;

//未使用的arena的個數
static struct arena_object* unused_arena_objects = NULL;

//可用的arena的個數
static struct arena_object* usable_arenas = NULL;

//初始化須要申請的arena的個數
#define INITIAL_ARENA_OBJECTS 16

static struct arena_object*
new_arena(void)
{	
    //arena,一個arena_object結構體對象
    struct arena_object* arenaobj;
    uint excess;        /* number of bytes above pool alignment */
	
    //[1]:判斷是否須要擴充"未使用"的arena列表
    if (unused_arena_objects == NULL) {
        uint i;
        uint numarenas;
        size_t nbytes;
		
        //[2]:肯定本次須要申請的arena_object的個數,並申請內存
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        ...
        nbytes = numarenas * sizeof(*arenas);
        arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
        if (arenaobj == NULL)
            return NULL;
        arenas = arenaobj;
        ...
        /* Put the new arenas on the unused_arena_objects list. */
        //[3]:初始化新申請的arena_object,並將其放入"未使用"arena鏈表中
        for (i = maxarenas; i < numarenas; ++i) {
            arenas[i].address = 0;              /* mark as unassociated */
            arenas[i].nextarena = i < numarenas - 1 ?
                                   &arenas[i+1] : NULL;
        }

        /* Update globals. */
        unused_arena_objects = &arenas[maxarenas];
        maxarenas = numarenas;
    }

    /* Take the next available arena object off the head of the list. */
    //[4]:從"未使用"arena鏈表中取出一個"未使用"的arena
    assert(unused_arena_objects != NULL);
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    assert(arenaobj->address == 0);
    
    //[5]:申請arena管理的內存,這裏咱們說的arena指的是arena_object,簡寫了
    address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
    if (address == NULL) {
        /* The allocation failed: return NULL after putting the
         * arenaobj back.
         */
        arenaobj->nextarena = unused_arena_objects;
        unused_arena_objects = arenaobj;
        return NULL;
    }
    arenaobj->address = (uintptr_t)address;
	
    //調整個數
    ++narenas_currently_allocated;
    ++ntimes_arena_allocated;
    if (narenas_currently_allocated > narenas_highwater)
        narenas_highwater = narenas_currently_allocated;
    //[6]:設置poo集合的相關信息,這是設置爲NULL
    arenaobj->freepools = NULL;
    /* pool_address <- first pool-aligned address in the arena
       nfreepools <- number of whole pools that fit after alignment */
    arenaobj->pool_address = (block*)arenaobj->address;
    arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
    assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);
    //將pool的起始地址調整爲系統頁的邊界
    excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
    if (excess != 0) {
        --arenaobj->nfreepools;
        arenaobj->pool_address += POOL_SIZE - excess;
    }
    arenaobj->ntotalpools = arenaobj->nfreepools;

    return arenaobj;
}

所以咱們能夠看到,python首先會檢查當前"未使用"鏈表中是否還有"未使用"arena,檢查的結果將決定後續的動做。

若是在"未使用"鏈表中還存在未使用的arena,那麼python會從"未使用"arena鏈表中抽取一個arena,接着調整"未使用"鏈表,讓它和抽取的arena斷絕一切聯繫。而後python申請了一塊256KB大小的內存,將申請的內存地址賦給抽取出來的arena的address。咱們已經知道,arena中維護的是pool集合,這塊256KB的內存就是pool的容身之處,這時候arena就已經和pool集合創建聯繫了。這個arena已經具有了成爲"可用"內存的條件,該arena和"未使用"arena鏈表脫離了關係,就等着被"可用"arena鏈表接收了,不過何時接收呢?先別急

隨後,python在代碼的[6]處設置了一些arena用戶維護pool集合的信息。須要注意的是,python將申請到的256KB內存進行了處理,主要是放棄了一些內存,並將可以使用的內存邊界(pool_address)調整到了與系統頁對齊。而後經過arenaobj->freepools = NULL;將freepools設置爲NULL,這不奇怪,基於對freeblock的瞭解,咱們知道要等到釋放一個pool時,這個freepools纔會有用。最後咱們看到,pool集合佔用的256KB內存在進行邊界對齊後,實際是交給pool_address來維護了。

回到new_arena中的[1]處,若是unused_arena_objects爲NULL,則代表目前系統中已經沒有"未使用"arena了,那麼python首先會擴大系統的arena集合(小塊內存內存池)。python在內部經過一個maxarenas的變量維護了存儲arena的數組的個數,而後在[2]處將待申請的arena的個數設置爲固然arena個數(maxarenas)的2倍。固然首次初始化的時候maxarenas爲0,此時爲16。

在得到了新的maxarenas後,python會檢查這個新獲得的值是否溢出了。若是檢查順利經過,python就會在[3]處經過realloc擴大arenas指向的內存,並對新申請的arena_object進行設置,特別是那個不起眼的address,要將新申請的address一概設置爲0。實際上,這是一個標識arena是出於"未使用"狀態仍是"可用"狀態的重要標記。而一旦arena(arena_object)和pool集合創建了聯繫,這個address就變成了非0,看代碼的[6]處。固然別忘記咱們爲何會走到[3]這裏,是由於unused_arena_objects == NULL了,並且最後還設置了unused_arena_objects,這樣系統中又有了"未使用"的arena了,接下來python就在[4]處對一個arena進行初始化了。

17.2.4 內存池

可用pool緩衝池--usedpools

經過#define SMALL_REQUEST_THRESHOLD 512咱們知道python內部默認的小塊內存與大塊內存的分界點定在512個字節。也就是說,當申請的內存小於512個字節,pymalloc_alloc會在內存池中申請內存,而當申請的內存超過了512字節,那麼pymalloc_alloc將退化爲malloc,經過操做系統來申請內存。固然,經過修改python源代碼咱們能夠改變這個值,從而改變python的默認內存管理行爲。

當申請的內存小於512字節時,python會使用area所維護的內存空間。那麼python內部對於area的個數是否有限制呢?換句話說,python對於這個小塊空間內存池的大小是否有限制?其實這個決策取決於用戶,python提供了一個編譯符號,用於控制是否限制內存池的大小,不過這裏不是重點,只須要知道就行。

儘管咱們在前面花了很多篇幅介紹arena,同時也看到arena是python的小塊內存池的最上層結構,全部arena的集合實際就是小塊內存池。然而在實際的使用中,python並不直接與arenas和arena數組打交道。當python申請內存時,最基本的操做單元並非arena,而是pool。估計到這裏懵了,別急,慢慢來。

舉個例子,當咱們申請一個28字節的內存時,python內部會在內存池尋找一塊可以知足需求的pool,從中取出一個block返回,而不會去尋找arena。這其實是由pool和arena的屬性決定的,在python中,pool是一個有size概念的內存管理抽象體,一個pool中的block老是有肯定的大小,這個pool老是和某個size class index對應,還記得pool_header中的那個szidx麼?而arena是沒有size概念的內存管理抽象體。這就意味着,同一個arena在某個時刻,其內部的pool集合可能都是32字節的block;而到了另外一個時刻,因爲系統須要,這個arena可能被從新劃分,其中的pool集合可能改成64字節的block了,甚至pool集合中通常的pool管理32字節,另外一半管理64字節。這就決定了在進行內存分配和銷燬時,全部的動做都是在pool上完成的。

固然內存池中的pool不只僅是一個有size概念的內存管理抽象體,更進一步的,它仍是一個有狀態的內存管理抽象體。一個pool在python運行的任何一個時刻,老是處於一下三種狀態中的一種:

used狀態:pool中至少有一個block已經被使用,而且至少有一個block未被使用。這種狀態的pool受控於python內部維護的usedpools數組。

full狀態:pool中全部的block都已經被使用,這種狀態的pool在arena中,可是再也不arena的freepools鏈表中。

empty狀態:pool中全部的block都未被使用,處於這個狀態的pool的集合經過其pool_header中的nextpool構成一個鏈表,這個鏈表的表頭就是arena中的freepools。

請注意:arena中處於full狀態的pool是各自獨立,沒有像其餘狀態的pool同樣,鏈接成一個鏈表。

咱們從圖中看到全部的處於used狀態的pool都被置於usedpools的控制之下。python內部維護的usedpools數組是一個很是巧妙的實現,維護着全部的處於used狀態的pool。當申請內存時,python就會經過usedpools尋找到一個可用的pool(處於used狀態),從中分配一個block。所以咱們想,必定有一個usedpools相關聯的機制,完成從申請的內存的大小到size class index之間的轉換,不然python就沒法找到最合適的pool了。這種機制和usedpools的結構有着密切的關係,咱們看一下它的結構。

//obmalloc.c
typedef uint8_t block;
#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)

//NB_SMALL_SIZE_CLASSES以前好像出現過,可是不用說也知道這表示當前配置下有多少個不一樣size的塊
//在我當前的機器就是512/8=64個,對應的size class index就是從0到63
#define NB_SMALL_SIZE_CLASSES   (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8
    , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
#if NB_SMALL_SIZE_CLASSES > 16
    , PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
#if NB_SMALL_SIZE_CLASSES > 24
    , PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
#if NB_SMALL_SIZE_CLASSES > 32
    , PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
#if NB_SMALL_SIZE_CLASSES > 40
    , PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
#if NB_SMALL_SIZE_CLASSES > 48
    , PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
#if NB_SMALL_SIZE_CLASSES > 56
    , PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
#if NB_SMALL_SIZE_CLASSES > 64
#error "NB_SMALL_SIZE_CLASSES should be less than 64"
#endif /* NB_SMALL_SIZE_CLASSES > 64 */
#endif /* NB_SMALL_SIZE_CLASSES > 56 */
#endif /* NB_SMALL_SIZE_CLASSES > 48 */
#endif /* NB_SMALL_SIZE_CLASSES > 40 */
#endif /* NB_SMALL_SIZE_CLASSES > 32 */
#endif /* NB_SMALL_SIZE_CLASSES > 24 */
#endif /* NB_SMALL_SIZE_CLASSES > 16 */
#endif /* NB_SMALL_SIZE_CLASSES >  8 */
};

感受這個數組有點怪異,別急咱們來畫圖看一看

考慮一下當申請28字節的情形,前面咱們說到,python首先會得到size class index,顯然這裏是3。那麼在usedpools中,尋找第3+3=6個元素,發現usedpools[6]的值是指向usedpools[4]的地址。好暈啊,好吧,如今對照pool_header的定義來看一看usedpools[6] -> nextpool這個指針指向哪裏了呢?

//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* 固然pool裏面的block數量    */
    block *freeblock;                   /* 一個鏈表,指向下一個可用的block   */
    struct pool_header *nextpool;       /* 指向下一個pool  */
    struct pool_header *prevpool;       /* 指向上一個pool       ""        */
    uint arenaindex;                    /* 在area裏面的索引 */
    uint szidx;                         /* block的大小(固定值?後面說)     */
    uint nextoffset;                    /* 下一個可用block的內存偏移量         */
    uint maxnextoffset;                 /* 最後一個block距離開始位置的距離     */
};

顯然是從usedpools[6](即usedpools+4)開始向後偏移8個字節(一個ref的大小加上一個freeblock的大小)後的內存,正好是usedpools[6]的地址(即usedpools+6),這是python內部的trick

想象一下,當咱們手中有一個size class爲32字節的pool,想要將其放入這個usedpools中時,要怎麼作呢?從上面的描述咱們知道,只須要進行usedpools[i+i] -> nextpool = pool便可,其中i爲size class index,對應於32字節,這個i爲3.當下次須要訪問size class 爲32字節(size class index爲3)的pool時,只須要簡單地訪問usedpools[3+3]就能夠獲得了。python正是使用這個usedpools快速地從衆多的pool中快速地尋找到一個最適合當前內存需求的pool,從中分配一塊block。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    poolp next;
    uint size;
    ...
    LOCK();
    //得到size class index
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    //直接經過usedpools[size+size],這裏的size不就是咱們上面說的i嗎?
    pool = usedpools[size + size];
    //若是usedpools中有可用的pool
    if (pool != pool->nextpool) {
        ... //有可用pool
    }
    ... //無可用pool,嘗試獲取empty狀態的pool
}

Pool的初始化

當python啓動以後,在usedpools這個小塊空間的內存池中,並不存在任何可用的內存,準確的說,不存在任何可用的pool。在這裏,python採用了延遲分配的策略,即當咱們確實開始申請小塊內存的時候,python才創建這個內存池。正如以前提到的,當咱們開始申請28字節的內存時,python實際將申請32字節的內存,而後會首先根據32字節對應的class size index(3)在usedpools中對應的位置查找,若是發如今對應的位置後面沒有鏈接任何可用的pool,python會從"可用"arena鏈表中的第一個可用的arena中獲取的一個pool。不過須要注意的是,當前得到的arena中包含的這些pools中可能會具備不一樣的class size index。

想象一下,當申請32字節的內存時,從"可用"arena中取出一個pool用做32字節的pool。當下一次內存分配請求分配64字節的內存時,python能夠直接使用當前"可用"的arena的另外一個pool便可,正如咱們以前說的arena沒有size class的屬性,而pool纔有。

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
    block *bp;
    poolp pool;
    poolp next;
    uint size;
    ...
    LOCK();
    size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    pool = usedpools[size + size];
    //若是usedpools中有可用的pool
    if (pool != pool->nextpool) {
        ... //有可用pool
    }
    //無可用pool,嘗試獲取empty狀態的pool
    if (usable_arenas == NULL) {
        //嘗試申請新的arena,並放入"可用"arena鏈表
        usable_arenas = new_arena();
        if (usable_arenas == NULL) {
            goto failed;
        }
        usable_arenas->nextarena =
            usable_arenas->prevarena = NULL;
    }
    assert(usable_arenas->address != 0);

    //從可用arena鏈表中第一個arena的freepools中抽取一個可用的pool
    pool = usable_arenas->freepools;
    if (pool != NULL) {
        /* Unlink from cached pools. */
        usable_arenas->freepools = pool->nextpool;
        //調整可用arena鏈表中第一個arena中的可用pool的數量
        --usable_arenas->nfreepools;
        //若是調整以後變爲0,則將該arena從可用arena鏈表中移除
        if (usable_arenas->nfreepools == 0) {
            /* Wholly allocated:  remove. */
            assert(usable_arenas->freepools == NULL);
            assert(usable_arenas->nextarena == NULL ||
                   usable_arenas->nextarena->prevarena ==
                   usable_arenas);

            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
                assert(usable_arenas->address != 0);
            }
        }
        else {
            /* nfreepools > 0:  it must be that freepools
             * isn't NULL, or that we haven't yet carved
             * off all the arena's pools for the first
             * time.
             */
            assert(usable_arenas->freepools != NULL ||
                   usable_arenas->pool_address <=
                   (block*)usable_arenas->address +
                       ARENA_SIZE - POOL_SIZE);
        }

    init_pool:
    	...
}

能夠看到,若是開始時"可用"arena鏈表爲空,那麼python會經過new_arena申請一個arena,開始構建"可用"arena鏈表。還記得咱們以前遺留了一個問題嗎?答案就在這裏。在這裏,一個脫離了"未使用"arena鏈表並轉變爲"可用"的arena被歸入了"可用"arena鏈表的控制。因此python會嘗試從"可用"arena鏈表中的第一個arena所維護的pool集合中取出一個可用的pool。若是成功地取出了這個pool,那麼python就會進行一些維護信息的更新工做,甚至在當前arena中可用的pool已經用完了以後,將該arena從"可用"arena鏈表中移除

好了,如今咱們手裏有了一塊用於32字節內存分配的pool,爲了提升之後內存分配的效率,咱們須要將這個pool放入到usedpools中。這一步就是咱們上面代碼中沒貼的init

//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
	init_pool:
    	//將pool放入usedpools中
        next = usedpools[size + size]; /* == prev */
        pool->nextpool = next;
        pool->prevpool = next;
        next->nextpool = pool;
        next->prevpool = pool;
        pool->ref.count = 1;
    	//pool在以前就具備正確的size結構,直接返回pool中的一個block
        if (pool->szidx == size) {
            bp = pool->freeblock;
            assert(bp != NULL);
            pool->freeblock = *(block **)bp;
            goto success;
        }
        //	pool以前就具備正確的size結果,直接返回pool中的一個block
        pool->szidx = size;
        size = INDEX2SIZE(size);
        bp = (block *)pool + POOL_OVERHEAD;
        pool->nextoffset = POOL_OVERHEAD + (size << 1);
        pool->maxnextoffset = POOL_SIZE - size;
        pool->freeblock = bp + size;
        *(block **)(pool->freeblock) = NULL;
        goto success;
    }
}

具體的細節能夠本身觀察源代碼去研究,這裏再也不寫了,有點累

block的釋放

考察完了對block的分配,是時候來看看對block的釋放了。對block的釋放實際上就是將一塊block歸還給pool,咱們已經知道pool可能存在3種狀態,在分別處於三種狀態,它們的位置是各不相同的。

當咱們釋放一個block以後,可能會引發pool狀態的轉變,這種轉變可分爲兩種狀況

  • used狀態轉變爲empty狀態
  • full狀態轉變爲used狀態
//obmalloc.c
static int
pymalloc_free(void *ctx, void *p)
{
    poolp pool;
    block *lastfree;
    poolp next, prev;
    uint size;
    pool = POOL_ADDR(p);
    if (!address_in_range(p, pool)) {
        return 0;
    }

    LOCK();
    assert(pool->ref.count > 0);            /* else it was empty */
    //設置離散自由的block鏈表
    *(block **)p = lastfree = pool->freeblock;
    pool->freeblock = (block *)p;
    //若是!lastfree成立,那麼意味着不存在lastfree,說明這個pool在釋放block以前是滿的
    if (!lastfree) {
        /* Pool was full, so doesn't currently live in any list:
         * link it to the front of the appropriate usedpools[] list.
         * This mimics LRU pool usage for new allocations and
         * targets optimal filling when several pools contain
         * blocks of the same size class.
         */
        //當前pool處於full狀態,在釋放一塊block以後,須要將其轉換爲used狀態
        //並從新鏈入到usedpools的頭部
        --pool->ref.count;
        assert(pool->ref.count > 0);            /* else the pool is empty */
        size = pool->szidx;
        next = usedpools[size + size];
        prev = next->prevpool;
        pool->nextpool = next;
        pool->prevpool = prev;
        next->prevpool = pool;
        prev->nextpool = pool;
        goto success;
    }

    struct arena_object* ao;
    uint nf;  /* ao->nfreepools */
	
    //不然到這一步表示lastfree有效
    //pool回收了一個block以後,不須要從used狀態轉換爲empty狀態
    if (--pool->ref.count != 0) {
        /* pool isn't empty:  leave it in usedpools */
        goto success;
    }
    /* Pool is now empty:  unlink from usedpools, and
     * link to the front of freepools.  This ensures that
     * previously freed pools will be allocated later
     * (being not referenced, they are perhaps paged out).
     */
    //不然說明pool爲空
    next = pool->nextpool;
    prev = pool->prevpool;
    next->prevpool = prev;
    prev->nextpool = next;
	
    //將pool放入freepools維護的鏈表中
    ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    nf = ++ao->nfreepools;

    if (nf == ao->ntotalpools) {
        //調整usable_arenas鏈表
        if (ao->prevarena == NULL) {
            usable_arenas = ao->nextarena;
            assert(usable_arenas == NULL ||
                   usable_arenas->address != 0);
        }
        else {
            assert(ao->prevarena->nextarena == ao);
            ao->prevarena->nextarena =
                ao->nextarena;
        }
        /* Fix the pointer in the nextarena. */
        if (ao->nextarena != NULL) {
            assert(ao->nextarena->prevarena == ao);
            ao->nextarena->prevarena =
                ao->prevarena;
        }
        //調整"未使用"arena鏈表
        ao->nextarena = unused_arena_objects;
        unused_arena_objects = ao;

        //程序走到這一步,表示是pool原先是used,釋放block以後依舊是used
        //那麼會將內存歸還給操做系統
        _PyObject_Arena.free(_PyObject_Arena.ctx,
                             (void *)ao->address, ARENA_SIZE);
        //設置address,將arena的狀態轉爲"未使用"
        ao->address = 0;                        /* mark unassociated */
        --narenas_currently_allocated;

        goto success;
    }
}

實際上在python2.4以前,python的arena是不會釋放pool的。這樣的話就會引發內存泄漏,好比咱們申請10 * 1024 * 1024個16字節的小內存,這就意味着必須使用160MB的內存,因爲python會默認所有使用arena(這一點咱們沒有提)來知足你的需求。可是當咱們將全部16字節的內存所有釋放了,這些內存也會回到arena的控制之中,這都沒有問題。可是問題來了,這些內存是被arena控制的,並無交給操做系統啊,,因此這160MB的內存始終會被python佔用,若是後面程序不再須要160MB如此巨大的內存,那麼不就浪費了嗎?

因爲這種狀況必須在大量持續申請小內存對象時纔會出現,由於大的話會自動交給操做系統了,小的纔會由arena控制,而持續申請大量小內存的狀況幾乎不會碰到,因此這個問題也就留在了 Python中。可是由於有些人發現了這個問題,因此這個問題在python2.5的時候就獲得瞭解決。

由於早期的python,arena是沒有區分"未使用"和"可用"兩種狀態的,到了python2.5中,arena已經能夠將本身維護的pool集合釋放,交給操做系統了,從而將"可用"狀態轉化爲"未使用"狀態。而當python處理完pool,就開始處理arena了。

而對arena的處理實際上分爲了4中狀況

  • 1.若是arena中全部的pool都是empty的,釋放pool集合所佔用的內存
  • 2.若是以前arena中沒有了empty的pool,那麼在"可用"鏈表中就找不到該arena,因爲如今arena中有了一個pool,因此須要將這個arena鏈入到"可用"鏈表的表頭
  • 3.若是arena中的empty的pool的個數爲n,那麼會從"可用"arena鏈表中開始尋找arena能夠插入的位置,將arena插入到"可用"鏈表。這樣操做的緣由就在於"可用"arena鏈表其實是一個有序的鏈表,從表頭開始日後,每個arena中empty的pool的個數,即nfreepools,都不能大於前面的arena,也不能小於後面的arena。保持這樣有序性的原則是分配block時,是從"可用"鏈表的表頭開始尋找可用arena的,這樣就能保證若是一個arena的empty pool數量越多,它被使用的機會就越少。所以它最終釋放其維護的pool集合的內存的機會就越大,這樣就能保證多餘的內存會被歸還給操做系統
  • 4.其餘狀況,則不對arena進行任何處理。

內存池全景

前面咱們已經提到了,對於一個用c開發的龐大的軟件(python是一門高級語言,可是執行對應代碼的解釋器則能夠當作是c的一個軟件),其中的內存管理可謂是最複雜、最繁瑣的地方了。不一樣尺度的內存會有不一樣的抽象,這些抽象在各類狀況下會組成各式各樣的鏈表,很是複雜。可是咱們仍是有可能從一個總體的尺度上把握整個內存池,儘管不一樣的鏈表變幻無常,但咱們只需記住,全部的內存都在arenas(或者說那個存放多個arena的數組)的掌握之中 。

17.3 循環引用之垃圾回收

17.3.1 引用計數之垃圾回收

如今絕大部分語言都實現了垃圾回收機制,也包括python。然而python的垃圾回收和java,c#等語言有一個很大的不一樣,那就是python中大多數對象的生命週期是經過對象的引用計數來管理的,這一點在開始的章節咱們就說了,對於python中最基礎的對象PyObject,有兩個屬性,一個是該對象的類型,還有一個就是引用計數(ob_refcnt)。不過從廣義上將,引用計數也算是一種垃圾回收機制,並且它是一中最簡單最直觀的垃圾回收計數。儘管須要一個值來維護引用計數,可是引用計數有一個最大的優勢:實時性。任何內存,一旦沒有指向它的引用,那麼就會被回收。而其餘的垃圾回收技術必須在某種特定條件下(好比內存分配失敗)才能進行無效內存的回收。

引用計數機制所帶來的維護引用計數的額外操做,與python運行中所進行的內存分配、釋放、引用賦值的次數是成正比的。這一點,相對於主流的垃圾回收技術,好比標記--清除(mark--sweep)、中止--複製(stop--copy)等方法相比是一個弱點,由於它們帶來額外操做只和內存數量有關,至於多少人引用了這塊內存則不關心。所以爲了與引用計數搭配、在內存的分配和釋放上得到最高的效率,python設計了大量的內存池機制,好比小整數對象池、字符串的intern機制,列表的freelist緩衝池等等,這些大量使用的面向特定對象的內存池機制正是爲了彌補引用計數的軟肋。

其實對於如今的cpu和內存來講,上面的問題都不是什麼問題。可是引用計數還存在一個致命的缺陷,這一缺陷幾乎將引用計數機制在垃圾回收技術中判處了"死刑",這一技術就是"循環引用"。並且也正是由於"循環引用"這個致命傷,致使在狹義上並不把引用計數機制當作是垃圾回收技術

在介紹循環引用以前,先來看看python引用計數何時會增長,何時會減小。

引用計數加一

  • 對象被建立:a=1
  • 對象被引用:b=a
  • 對象被做爲參數傳到一個函數中,func(a)
  • 對象做爲列表、元組等其餘容器裏面的元素

引用計數減一

  • 對象別名被顯式的銷燬:del a
  • 對象的引用指向了其餘的對象:a=2
  • 對象離開了它的做用域,好比函數的局部變量,在函數執行完畢的時候,也會被銷燬(若是沒有獲取棧幀的話),而全局變量則不會
  • 對象所在的容器被銷燬,或者從容器中刪除等等

查看引用計數

查看一個對象的引用計數,能夠經過sys.getrefcount(obj),可是因爲做爲getrefcount這個函數的參數,因此引用計數會多1。

咱們以前說,a = "mashiro",至關於把a和a對應的值組合起來放在了命名空間裏面,那麼你認爲這個a對應的值是什麼呢?難道是"mashiro"這個字符串嗎?其實從python的層面上來看的話確實是這樣,可是在python的底層,其實存儲的是字符數組"mashiro"對應地址,我總以爲前面章節好像說錯了。

b=a在底層中則表示把a的指針拷貝給了b,是的你沒有看錯,都說python傳遞的是符號,可是在底層就是傳遞了一個指針,不管什麼傳遞的都是指針,在python的層面上傳遞就是符號、或者就是引用。因此咱們看到, 每當多了一個引用,那麼"mashiro"(在c的層面上是一個結構體,PyUnicodeObject)的引用計數就會加1.

而每當減小一個引用,引用計數就會減小1。儘管咱們用sys.getrefcount獲得的結果是2,可是當這個函數執行完,因爲局部變量的銷燬,其實結果已經變成了1。所以引用計數很方便,就是當一片空間沒有人引用了,那麼就直接銷燬。儘管維護這個引用計數須要消耗資源,可仍是那句話,對於現在的硬件資源來講,是徹底能夠接受的,畢竟引用計數真的很方便。可是,是的我要說可是了,就是咱們以前的那個循環引用的問題。

l1 = []
l2 = []

l1.append(l2)
l2.append(l1)

del l1, l2

初始的時候,l1和l2指向的內存的引用計數都爲1,可是l1.append(l2),那麼l2指向內存的引用計數變成了2,同理l2.append(l1)致使l1指向內存的引用計數也變成了2。所以當咱們del l1, l2的時候,引用計數會從2變成1,所以l1和l2都不會被回收,由於咱們是但願回收l1和l2的,可是若是隻有引用計數的話,那麼顯然這二者是回收不了的。所以這算是引用計數的最大的缺陷,由於會致使內存泄漏。所以python爲了解決這個問題,就必須在引用計數機制之上又引入了新的主流垃圾回收計數:標記--清除和分代收集計數來彌補這個最致命的漏洞。

17.3.2 三色標記模型

不管何種垃圾回收機制,通常都分爲兩個階段:垃圾檢測和垃圾回收。垃圾檢測是從全部的已經分配的內存中區別出"可回收"和"不可回收"的內存,而垃圾回收則是使操做系統從新掌握垃圾檢測階段所標識出來的"可回收"內存塊。因此垃圾回收,並非說直接把這塊內存的數據清空了,而是說將使用權重新交給了操做系統,不會本身霸佔了。下面咱們來看看標記--清除(mark--sweep)方法是如何實現的,併爲這個過程創建一個三色標記模型,python中的垃圾回收正是基於這個模型完成的。

從具體的實現上來說,標記--清除方法一樣遵循垃圾回收的兩個階段,其簡要過程以下:

  • 尋找根對象(root object)的集合,所謂的root object就是一些全局引用和函數棧的引用。這些引用所用的對象是不可被刪除的,而這個root object集合也是垃圾檢測動做的起點
  • 從root object集合出發,沿着root object集合中的每個引用,若是能到達某個對象A,則稱A是可達的(reachable),可達的對象也不可被刪除。這個階段就是垃圾檢測階段
  • 當垃圾檢測階段結束後,全部的對象分爲了可達的(reachable)和不可達的(unreachable)。而全部可達對象都必須予以保留,而不可達對象所佔用的內存將被回收。

在垃圾回收動做被激活以前,系統中所分配的全部對象和對象之間的引用組成了一張有向圖,其中對象是圖中的節點,而對象間的引用則是圖的邊。咱們在這個有向圖的基礎之上創建一個三個標註模型,更形象的展現垃圾回收的整個動做。當垃圾回收開始時,咱們假設系統中的全部對象都是不可達的,對應在有向圖上就是白色 。隨後從垃圾回收的動做開始,沿着始於root object集合中的某個object的引用鏈,在某個時刻到達了對象A,那咱們把A標記爲灰色,灰色表示一個對象是可達的,可是其包含的引用尚未被檢查。當咱們檢查了對象A所包含的全部引用以後,A將被標記爲黑色,表示其包含的全部引用已經被檢查過了。顯然,此時A中引用的對象則被標記成了灰色。假如咱們從root object集合開始,按照廣度優先的策略進行搜索的話,那麼不難想象,灰色節點對象集合就如同波紋同樣,不斷向外擴散,隨着全部的灰色節點都變成了黑色節點,也就意味着垃圾檢測階段結束了。

17.4 python中的垃圾回收

如以前所說,python中主要的內存管理手段是引用計數機制,而標記--清除和分代收集只是爲了打破循環引用而引入的補充技術。這一事實意味着python中的垃圾回收只關注可能會產生循環引用的對象,而像PyLongObject、PyUnicodeObject這些對象是絕對不可能產生循環引用的,由於它們內部不可能持有對其餘對象的引用,因此這些直接經過引用計數機制就能夠實現,並且後面咱們說的垃圾回收也專指那些可能產生循環引用的對象。python中的循環引用只會老是發生在container對象之間,所謂container對象就是內部可持有對其餘對象的引用的對象,好比list、dict、class、instance等等。當python開始垃圾回收機制開始運行時,只須要檢查這些container對象,而對於PyLongObject、PyUnicodeObject則不須要理會,這使得垃圾回收帶來的開銷只依賴於container對象的數量,而非全部對象的數量。爲了達到這一點,python就必須跟蹤所建立的每個container對象,並將這些對象組織到一個集合中,只有這樣,才能將垃圾回收的動做限制在這些對象上。而python採用了一個雙向鏈表,全部的container對象在建立以後,都會被插入到這個鏈表當中。

17.4.1 可收集對象鏈表

在對python對象機制的分析當中咱們已經看到,任何一個python對象均可以分爲兩部分,一部分是PyObject_HEAD,另外一部分是對象自身的數據。然而對於一個須要被垃圾回收機制跟蹤的container來講,還不夠,由於這個對象還必須鏈入到python內部的可收集對象鏈表中。而一個container對象要想成爲一個可收集的對象,則必須加入額外的信息,這個信息位於PyObject_HEAD以前,稱爲PyGC_Head

//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;  /* force worst-case alignment */
    // malloc returns memory block aligned for any built-in types and
    // long double is the largest standard C type.
    // On amd64 linux, long double requires 16 byte alignment.
    // See bpo-27987 for more discussion.
} PyGC_Head;

因此,對於python所建立的可收集container對象,其內存分佈與咱們以前所瞭解的內存佈局是不一樣的,咱們能夠從可收集container對象的建立過程當中窺見其內存分佈。

//Modules/gcmodule.c
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

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

#define GC_UNTRACKED                    _PyGC_REFS_UNTRACKED
#define _PyGC_REFS_UNTRACKED                    (-2) //該行位於objimpl.h中

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    //將對象和PyGC_Head所需內存加起來
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    //爲對象自己和PyGC_Head申請內存
    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);
    _PyRuntime.gc.generations[0].count++; /* number of allocated GC objects */
    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;
}

所以咱們能夠很清晰的看到,當python爲可收集的container對象申請內存空間時,爲PyGC_Head也申請了空間,而且其位置位於container對象以前。因此對於PyListObject、PyDictObject等container對象的內存分佈的推測就應該變成這樣。

在可收集container對象的內存分佈中,內存分爲三個部分,首先第一塊用於垃圾回收機制,而後緊跟着的是python中全部對象都會有的PyObject_HEAD,最後纔是container自身的數據。這裏的container對象,既能夠是PyDictObject、也能夠是PyListObject等等。

//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;  /* force worst-case alignment */
    // malloc returns memory block aligned for any built-in types and
    // long double is the largest standard C type.
    // On amd64 linux, long double requires 16 byte alignment.
    // See bpo-27987 for more discussion.
} PyGC_Head;

再來看看PyGC_Head的模樣,裏面除了兩個創建鏈表結構的前向和後向指針外,還有一個gc_ref,而這個值被初始化爲GC_UNTRACKED,在上面的代碼中能夠看到。這個變量對於垃圾回收的運行相當重要,可是在分析它以前咱們還須要瞭解一些其餘的東西。

當垃圾回收機制運行期間,咱們須要在一個可收集的container對象的PyGC_Head部分和PyObject_HEAD部分之間來回切換。更清楚的說,某些時候,咱們持有一個對象A的PyObject_HEAD的地址,可是咱們須要根據這個地址來得到PyGC_Head的地址;並且某些時候,咱們又須要反過來進行逆運算。而python提供了兩個地址之間的轉換算法

//gcmodule.c
//AS_GC,根據PyObject_HEAD獲得PyGC_Head
#define AS_GC(o) ((PyGC_Head *)(o)-1)
//FROM_GC,從PyGC_Head那裏獲得PyObject_HEAD
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))

//objimpl.h
#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)

在PyGC_Head中,出現了用於創建鏈表的兩個指針,只有將建立的可收集container對象連接到python內部維護的可收集對象鏈表中,python的垃圾回收機制才能跟蹤和處理這個container對象。可是咱們發現,在建立可收集container對象之時,並無馬上將這個對象鏈入到鏈表中。實際上,這個動做是發生在建立某個container對象最後一步,以PyListObject的建立舉例。

//listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
    PyListObject *op;
    ...
    Py_SIZE(op) = size;
    op->allocated = size;
    //建立PyListObject對象、並設置完屬性以後,返回以前,經過這一步_PyObject_GC_TRACK將所建立的container對象連接到了python中的可收集對象鏈表中。
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}

//objimpl.h
#define _PyObject_GC_TRACK(o) do { \
    PyGC_Head *g = _Py_AS_GC(o); \
    if (_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED) \
        Py_FatalError("GC object already tracked"); \
    _PyGCHead_SET_REFS(g, _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);

前面咱們說過,python會將本身的垃圾回收機制限制在其維護的可收集對象鏈表上,由於全部的循環引用必定是發生這個鏈表的一羣對象之間。在_PyObject_GC_TRACK以後,咱們建立的container對象也就置身於python垃圾回收機制的掌控機制當中了。

一樣的,python還提供將一個container對象從鏈表中摘除的方法,顯然這個方法應該會在對象被銷燬的時候調用。

//objimpl.h
#define _PyObject_GC_UNTRACK(o) do { \
    PyGC_Head *g = _Py_AS_GC(o); \
    assert(_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED); \
    _PyGCHead_SET_REFS(g, _PyGC_REFS_UNTRACKED); \
    g->gc.gc_prev->gc.gc_next = g->gc.gc_next; \
    g->gc.gc_next->gc.gc_prev = g->gc.gc_prev; \
    g->gc.gc_next = NULL; \
    } while (0);

很明顯,_PyObject_GC_UNTRACK只是_PyObject_GC_TRACK的逆運算而已

17.4.2 分代的垃圾收集

不管什麼語言,寫出來的程序都有共同之處。那就是不一樣對象的聲明週期會存在不一樣,有的對象所佔的內存塊的生命週期很短,而有的內存塊的生命週期則很長,甚至可能從程序的開始持續到程序結束。這二者的比例大概在80~90%

這對於垃圾回收機制有着重要的意義,由於咱們已經知道,像標記--清除這樣的垃圾回收機制所帶來的額外操做其實是和系統中內存塊的數量是相關的,當須要回收的內存塊越多的時候,垃圾檢測帶來的額外操做就越多,相反則越少。所以咱們能夠採用一種空間換時間的策略,由於目前全部對象都在一個鏈子上,每當進行垃圾回收機制的時候,都要把全部對象都檢查一遍。而其實也有很多比較穩定的對象(在屢次垃圾回收的洗禮下能活下來),咱們徹底沒有必要每次都檢查,或者說檢查的頻率能夠下降一些。因而聰明如你已經猜到了,咱們再來一根鏈子不就能夠了,把那些認爲比較穩定的對象移到另一條鏈子上,而新的鏈子進行垃圾回收的頻率會低一些,總之頻率不會像初始的鏈子那麼高。

因此這種思想就是:將系統中的全部內存塊根據其存活時間劃分爲不一樣的集合,每個集合就成爲一個"代",垃圾回收的頻率隨着"代"的存活時間的增大而減少,也就是說,存活的越長的對象就越可能不是垃圾,就越多是程序中須要一直存在的對象,就應該少去檢測它。反正不是垃圾,你檢了也白檢。那麼關鍵的問題來了,這個存活時間是如何被衡量的呢?或者咱們說當對象比較穩定的時候的這個穩定是如何衡量的呢?沒錯,咱們上面已經暴露了,就是經過經歷了幾回垃圾回收動做來評判,若是一個對象經歷的垃圾回收次數越多,那麼顯然其存活時間就越長。由於python的垃圾回收器,每當條件知足時(至於什麼條件咱們後面會說),就會進行一次垃圾回收(注意:不一樣的代的垃圾回收的頻率是不一樣的),而每次掃黃的時候你都不在,吭,每次垃圾回收的時候你都能活下來,這就說明你存活的時間更長,或者像咱們上面說的更穩定,那麼就不該該再把你放在這個鏈子上了,而是會移動到新的鏈子上。而在新的鏈子上,進行垃圾回收的頻率會下降,由於既然穩定了,檢測就沒必要那麼頻繁了,或者說新的鏈子上觸發垃圾回收所須要的時間更長了。

"代"彷佛是一個比較抽象的概念,但在python中,你就把"代"想象成多個對象組成集合,或者你把"代"想象成鏈表(或者鏈子)也能夠,由於這些對象都串在鏈表上面。而屬於同一"代"的內存塊都被連接在同一個鏈表中。而在python中總共存在三條鏈表,說明python中全部的對象總共能夠分爲三代,分別零代、一代、二代。一個"代"就是一條咱們上面提到的可收集對象鏈表。而在前面所介紹的鏈表的基礎之上,爲了支持分代機制,咱們須要的僅僅是一個額外的表頭而已。

//Include/internal/mem.h
struct gc_generation {
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
                  generations */
};
#define NUM_GENERATIONS 3

//gcmodule.c
#define GEN_HEAD(n) (&_PyRuntime.gc.generations[n].head)
    struct gc_generation generations[NUM_GENERATIONS] = {
        /* PyGC_Head,                                 threshold,      count */
        {{{_GEN_HEAD(0), _GEN_HEAD(0), 0}},           700,            0},
        {{{_GEN_HEAD(1), _GEN_HEAD(1), 0}},           10,             0},
        {{{_GEN_HEAD(2), _GEN_HEAD(2), 0}},           10,             0},
    };
	state->generation0 = GEN_HEAD(0);

上面這個維護了三個gc_generation結構的數組,經過這個數組控制了三條可收集對象鏈表,這就是python中用於分代垃圾收集的三個"代"。

而咱們在以前上面說的_PyObject_GC_TRACK中會看到_PyGC_generation0,它不偏不斜,指向的正是第0代鏈表。

對於每個gc_generation,其中的count記錄了當前這條可收集對象鏈表中一共有多少個對象。而在_PyObject_GC_Alloc中咱們能夠看到每當分配了內存,就會進行_PyRuntime.gc.generations[0].count++動做,將第0代鏈表中所維護的內存塊數量加1,這預示着全部新建立的對象實際上都會被加入到0代鏈表當中,而這一點也確實如此,已經被_PyObject_GC_TRACK證實了。並且咱們發現這裏是先將數量加1,而後再將新的container對象(內存塊)纔會被連接到第0代鏈表當中,固然這個無所謂啦。

而gc_generation中的threshold則記錄該條可收集對象鏈表中最多能夠容納多少個可收集對象,從python的實現代碼中,咱們知道第0代鏈表中最多能夠容納700個對象(只多是container對象)。而一旦第0代鏈表中的container對象超過了700個這個閾值,那麼會馬上除法垃圾回收機制。

static Py_ssize_t
collect_generations(void)
{
    int i;
    Py_ssize_t n = 0;
    for (i = NUM_GENERATIONS-1; i >= 0; i--) {
        //當count大於threshold的時候,可是這個僅僅針對於0代鏈表
        if (_PyRuntime.gc.generations[i].count > _PyRuntime.gc.generations[i].threshold) {
            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;
}

這裏面雖然寫了一個for循環,可是隻有當第0代鏈表的count超過了threshold的時候纔會觸發垃圾回收,那麼1代鏈表和2代鏈表觸發垃圾回收的條件又是什麼呢?當0代鏈表觸發了10次垃圾回收的時候,會觸發一次1代鏈表的垃圾回收。當1代鏈表觸發了10次垃圾回收的時候,會觸發一次2代鏈表的垃圾回收。另外:

在清理1代鏈表的時候,會順帶清理0代鏈表

在清理2代鏈表的時候,會順帶清理0代鏈表和1代鏈表

17.4.3 python中的標記--清除

咱們上面說到,當清理1代鏈表會順帶清理0代鏈表,老是就是把比本身"代"要小的鏈子也清理了。那麼這是怎麼作到的呢?其實答案就在gc_list_merge函數中,若是清理的是1代鏈表,那麼在開始垃圾回收以前,python會將0代鏈表(比它年輕的),整個地連接到1代鏈表以後。

//gcmodule.c
static void
gc_list_merge(PyGC_Head *from, PyGC_Head *to)
{
    PyGC_Head *tail;
    assert(from != to);
    if (!gc_list_is_empty(from)) {
        tail = to->gc.gc_prev;
        tail->gc.gc_next = from->gc.gc_next;
        tail->gc.gc_next->gc.gc_prev = tail;
        to->gc.gc_prev = from->gc.gc_prev;
        to->gc.gc_prev->gc.gc_next = to;
    }
    gc_list_init(from);
}

以咱們舉的例子來講的話,那麼這裏的from就是0代鏈表,to就是1代鏈表,因此此後的標記--清除算法就將在merge以後的那一條鏈表上進行。

在介紹python中的標記--清除垃圾回收方法以前,咱們須要創建一個循環引用的最簡單例子

list1 = []
list2 = []

list1.append(list2)
list2.append(list1)

# 注意這裏多了一個外部引用
a = list1

list3 = []
list4 = []
list3.append(list4)
list4.append(list3)

上面的數字指的是當前對象的引用計數ob_refcnt的值

17.4.3.1 尋找root object集合

爲了使用標記--清除算法,按照咱們以前對垃圾收集算法的通常性描述,首先咱們須要找到root object,那麼在咱們上面的那幅圖中,哪些是屬於root object呢?

讓咱們換個角度來思考,前面提到,root object是不能被刪除的對象。也就是說,在可收集對象鏈表的外部存在着某個引用在引用這個對象,刪除這個對象會致使錯誤的行爲,那麼在咱們當前這個例子中只有list1是屬於root object的。但這僅僅是觀察的結果,那麼如何設計一種算法來獲得這個結果呢?

咱們注意到這樣一個事實,若是兩個對象的引用計數都爲1,可是僅僅它們之間存在着循環引用,那麼這兩個對象是須要被回收的,也就是說,儘管它們的引用計數表現爲非0,可是實際上有效的引用計數爲0。這裏,咱們提出了有效引用計數的概念,爲了從引用計數中得到優秀的引用計數,必須將循環引用的影響取出,也就是說,這個閉環從引用中摘除,而具體的實現就是兩個對象各自的引用值都減去1。這樣一來,兩個對象的引用計數都成爲了0,這樣咱們便揮去了循環引用的迷霧,是有效引用計數出現了真身。那麼如何使兩個對象的引用計數都減1呢,很簡單,假設這兩個對象爲A和B,那麼從A出發,因爲它有一個對B的引用,則將B的引用計數減1;而後順着引用達到B,發現它有一個對A的引用,那麼一樣會將A的引用減1,這樣就完成了循環引用對象間環的刪除。

總結一下就是,python會尋找那些具備循環引用的、可是沒有被外部引用的對象,並嘗試把它們的引用計數都減去1

可是這樣就引出了一個問題,假設可收集對象鏈表中的container對象A有一個對對象C的引用,而C並不在這個鏈表中,若是將C的引用計數減去1,而最後A並無被回收,那麼顯然,C的引用計數被錯誤地減小1,這將致使將來的某個時刻對C的引用會出現懸空。這就要求咱們必須在A沒有被刪除的狀況下回復C的引用計數,但是若是採用這樣的方案的話,那麼維護引用計數的複雜度將成倍增加。換一個角度,其實咱們有更好的作法,咱們不改動真實的引用計數,而是改動引用計數的副本。對於副本,咱們不管作什麼樣的改動,都不會影響對象生命週期的維護,由於這個副本的惟一做用就是尋找root  object集合,而這個副本就是PyGC_Head中的gc.gc_ref。在垃圾回收的第一步,就是遍歷可收集對象鏈表,將每一個對象的gc.gc_ref的值設置爲其ob_refcnt的值。

//gcmodule.c
static void
update_refs(PyGC_Head *containers)
{
    PyGC_Head *gc = containers->gc.gc_next;
    for (; gc != containers; gc = gc->gc.gc_next) {
        assert(_PyGCHead_REFS(gc) == GC_REACHABLE);
        _PyGCHead_SET_REFS(gc, Py_REFCNT(FROM_GC(gc)));
        assert(_PyGCHead_REFS(gc) != 0);
    }
}

//而接下來的動做就是要將環引用從引用中摘除
static void
subtract_refs(PyGC_Head *containers)
{
    traverseproc traverse;
    PyGC_Head *gc = containers->gc.gc_next;
    for (; gc != containers; gc=gc->gc.gc_next) {
        traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
        (void) traverse(FROM_GC(gc),
                       (visitproc)visit_decref,
                       NULL);
    }
}

咱們注意到裏面有一個traverse,這個是和特定的container 對象有關的,在container對象的類型對象中定義。通常來講,traverse的動做就是遍歷container對象中的每個引用,而後對引用進行某種動做,而這個動做在subtract_refs中就是visit_decref,它以一個回調函數的形式傳遞到traverse操做中。好比:咱們來看看PyListObject對象所定義traverse操做。

//object.h
typedef int (*visitproc)(PyObject *, void *);
typedef int (*traverseproc)(PyObject *, visitproc, void *);

//listobject.c
PyTypeObject PyList_Type = {
    ...
    (traverseproc)list_traverse,                /* tp_traverse */
    ...
};

static int
list_traverse(PyListObject *o, visitproc visit, void *arg)
{
    Py_ssize_t i;

    for (i = Py_SIZE(o); --i >= 0; )
        //對列表中的每個元素都進行回調的操做
        Py_VISIT(o->ob_item[i]);
    return 0;
}

//gcmodule.c
/* A traversal callback for subtract_refs. */
static int
visit_decref(PyObject *op, void *data)
{
    assert(op != NULL);
    //PyObject_IS_GC判斷op指向的對象是否是被垃圾收集監控的
    //標識container對象是被垃圾收集監控的
    if (PyObject_IS_GC(op)) {
        PyGC_Head *gc = AS_GC(op);
        assert(_PyGCHead_REFS(gc) != 0); /* else refcount was too small */
        if (_PyGCHead_REFS(gc) > 0)
            _PyGCHead_DECREF(gc);
    }
    return 0;
}

在完成了subtract_refs以後,可收集對象鏈表中全部container對象之間的環引用就被摘除了。這時有一些container對象的PyGC_Head.gc_ref還不爲0,這就意味着存在對這些對象的外部引用,這些對象就是開始標記--清除算法的root object。

估計有人不明白引用計數是加在什麼地方,其實變量=值在python中,變量獲得的都是值的指針,a = 1,表示是在命名空間裏面會有"a": 1這個鍵值對,但看似是這樣,其實存儲的並非1,而是1這個結構體(python對象在底層是一個結構體)的指針,這個結構體存儲在堆區。咱們獲取a的引用計數,實際上是獲取a指向的這個對象的引用計數,此時爲1,若是b=a,在底層就等價於把a存儲的內容(指針)拷貝給了b,那麼此時a和b存儲的指針指的都是同一個對象,那麼這個對象的引用計數就變成了2。若是再來個b=2,那麼表示再建立一個結構體存儲的值爲2,而後讓b存儲新的結構體的指針。那麼原來的結構體的引用計數就從2又變成了1。

因此爲何初始的時候,list1的引用計數是3就很明顯了,list1的引用計數指的實際上是list1這個變量對應的值(或者說在底層,list1存儲的指針指向的值)的引用計數,因此一旦建立一個變量那麼引用計數會自動增長爲1,而後a也指向了list1所指向的內存,而且list1又做爲list2的一個元素(這個位置的元素存儲了指向list1的指針),因此引用計數總共是3。

因爲sys.getrefcount函數自己會多一個引用,因此減去1的話,那麼都是3。表示它們指向的內存存儲的值的引用計數爲3。sys.getrefcount(a) -> 4,這個時候a就想到了,除了我,還有兩位老鐵指向了我指向的內存。

17.4.3.2 垃圾標記

假設咱們如今執行了刪除操做del list1, list2, list3, list4,那麼成功地尋找到root object集合以後,咱們就能夠從root object觸發,沿着引用鏈,一個接一個地標記不能回收的內存,因爲root object集合中的對象是不能回收的,所以,被這些對象直接或間接引用的對象也是不能回收的,好比這裏的list2,即使del list2,可是由於list1不能回收,而又append了list2,因此list2指向的內存也是不能夠釋放的。下面在從root object出發前,咱們首先須要將如今的內存鏈表一分爲二,一條鏈表維護root object集合,成爲root鏈表,而另外一條鏈表中維護剩下的對象,成爲unreachable鏈表。之因此要分解成兩個鏈表,是出於這樣一種考慮:顯然,如今的unreachable鏈表是名存實亡的,由於裏面可能存在被root鏈表中的對象直接或者間接引用的對象,這些對象也是不能夠回收的,所以一旦在標記中發現了這樣的對象,那麼就應該將其從unreachable中移到root鏈表中;當完成標記以後,unreachable鏈表中剩下的對象就是名副其實的垃圾對象了,那麼接下來的垃圾回收只須要限制在unreachable鏈表中便可。

爲此python專門準備了一條名爲unreachable的鏈表,經過move_unreachable函數完成了對原始鏈表的切分。

//gcmodule.c
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *gc = young->gc.gc_next;
    while (gc != young) {
        PyGC_Head *next;
        //[1]:若是是root object
        if (_PyGCHead_REFS(gc)) {
            PyObject *op = FROM_GC(gc);
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            assert(_PyGCHead_REFS(gc) > 0);
            //設置其gc_refs爲GC_REACHABLE
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
            if (PyTuple_CheckExact(op)) {
                _PyTuple_MaybeUntrack(op);
            }
        }
        else {
            //[2]:對於非root object,移到unreachable鏈表中
            next = gc->gc.gc_next;
            gc_list_move(gc, unreachable);
            _PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE);
        }
        gc = next;
    }
}


static int
visit_reachable(PyObject *op, PyGC_Head *reachable)
{
    if (PyObject_IS_GC(op)) {
        PyGC_Head *gc = AS_GC(op);
        const Py_ssize_t gc_refs = _PyGCHead_REFS(gc);
        //[3]:對於尚未處理的對象,恢復其gc_refs
        if (gc_refs == 0) {
            _PyGCHead_SET_REFS(gc, 1);
        }
        //[4]:對於已經被挪到unreachable鏈表中的對象,將其再次挪動到原來的鏈表
        else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
            gc_list_move(gc, reachable);
            _PyGCHead_SET_REFS(gc, 1);
        }

         else {
            assert(gc_refs > 0
                   || gc_refs == GC_REACHABLE
                   || gc_refs == GC_UNTRACKED);
         }
    }
    return 0;
}

在move_unreachable中,沿着可收集對象鏈表依次向前,並檢查其PyGC_Head.gc.gc_ref值,咱們發現這裏的動做是遍歷鏈表,而並不是從root object集合出發,遍歷引用鏈。這會致使一個微妙的結果,即當檢查到一個gc_ref爲0的對象時,咱們並不能當即判定這個對象就是垃圾對象。由於在這個對象以後的對象鏈表上,也許還會遇到一個root object,而這個root object引用該對象。因此這個對象只是一個可能的垃圾對象,所以咱們纔要將其標誌爲GC_TENTATIVELY_UNREACHABLE,可是仍是經過gc_list_move將其搬到了unreachable鏈表中,咦,難道不會出問題嗎?別急,咱們立刻就會看到, python還留了後手。

當在move_unreachable中遇到一個gc_refs不爲0的對象A時,顯然,A是root object或者是從某個root object開始能夠引用到的對象,而A所引用的全部對象也都是不可回收的對象。所以在代碼的[1]處下面,咱們看到會再次調用與特定對象相關的transverse操做,依次對A所引用的對象調用visit_reachable。在visit_reachable的[4]處咱們發現,若是A所引用的對象以前曾被標註爲GC_TENTATIVELY_UNREACHABLE,那麼如今A能夠訪問到它,意味着它也是一個不可回收的對象,因此python會再次從unreachable鏈表中將其搬回到原來的鏈表。注意:這裏的reachable,就是move_unreachable中的young,也就是咱們所謂的root object鏈表。python還會將其gc_refs設置爲1,表示該對象是一個不可回收對象。一樣在[1]處,咱們看到對A所引用的gc_refs爲0的對象,其gc_refs也被設置成了1。想想這是什麼對象呢?顯然它就是在鏈表move_unreachable操做中尚未訪問到的對象,這樣python就直接掐斷了以後move_unreachable訪問它時將其移動到unreachable鏈表的誘因。

當move_unreachable完成以後,最初的一條鏈表就被切分紅了兩條鏈表,在unreachable鏈表中,就是咱們發現的垃圾對象,是垃圾回收的目標。可是等一等,在unreachable鏈表中,全部的對象均可以安全回收嗎?其實,垃圾回收在清理對象的時候,默認是會清理的,可是一旦當咱們定義了函數__del__,那麼在清理對象的時候就會調用這個__del__方法,所以也叫析構函數,這是python爲開發人員提供的在對象被銷燬時進行某些資源釋放的Hook機制。在python3中,即便咱們重寫了也沒事,由於python會把含有__del__函數的PyInstanceObject對象都通通移動到一個名爲garbage的PyListObject對象中。

17.4.4.4 垃圾回收

要回收unreachable鏈表中的垃圾對象,就必須先打破對象間的循環引用,前面咱們已經闡述瞭如何打破循環引用的辦法,下面來看看具體的銷燬過程

//gcmodule.c
static int
gc_list_is_empty(PyGC_Head *list)
{
    return (list->gc.gc_next == list);
}

static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)
{
    inquiry clear;

    while (!gc_list_is_empty(collectable)) {
        PyGC_Head *gc = collectable->gc.gc_next;
        PyObject *op = FROM_GC(gc);

        if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {
            PyList_Append(_PyRuntime.gc.garbage, op);
        }
        else {
            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                Py_INCREF(op);
                clear(op);
                Py_DECREF(op);
            }
        }
        if (collectable->gc.gc_next == gc) {
            /* object is still alive, move it, it may die later */
            gc_list_move(gc, old);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
        }
    }
}

其中會調用container對象的類型對象中的tp_clear操做,這個操做會調整container對象中引用的對象的引用計數值,從而打破完成循環的最終目標。仍是以PyListObject爲例:

//listobject.c
static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        i = Py_SIZE(a);
        //將ob_size調整爲0
        Py_SIZE(a) = 0;
        //ob_item是一個二級指針,原本指向一個數組的指針
        //如今指向爲NULL
        a->ob_item = NULL;
        //容量也設置爲0
        a->allocated = 0;
        while (--i >= 0) {
            //數組裏面元素也所有減小引用計數
            Py_XDECREF(item[i]);
        }
        //釋放數組
        PyMem_FREE(item);
    }
    return 0;
}

咱們注意到,在delete_garbage中,有一些unreachable鏈表中的對象會被從新送回到reachable鏈表(即delete_garbage的old參數)中,這是因爲進行clear動做時,若是成功進行,則一般一個對象會把本身從垃圾回收機制維護的鏈表中摘除(也就是這裏的collectable鏈表)。因爲某些緣由,對象可能在clear動做時,沒有成功完成必要的動做,從而沒有將本身從collectable鏈表摘除,這表示對象認爲本身還不能被銷燬,因此python須要講這種對象放回到reachable鏈表中。

咱們在上面看到了list_clear,假設是調用了list3的list_clear,那麼很差意思,這個是對list4作的處理。由於list3和list4存在循環引用,若是調用了list3的list_clear會減小list4的引用計數,因爲這兩位老鐵都被刪除了,還惺惺相惜賴在內存裏面不走,因此將list4的引用計數減小1以後,只能歸於湮滅了,而後會調用其list_dealloc,注意:這時候調用的是list4的list_dealloc。

//listobjct.c
static void
list_dealloc(PyListObject *op)
{
    Py_ssize_t i;
    //從可收集鏈表中移除
    PyObject_GC_UnTrack(op);
    Py_TRASHCAN_SAFE_BEGIN(op)
    if (op->ob_item != NULL) {
        //依次遍歷,減小內部元素的引用計數
        i = Py_SIZE(op);
        while (--i >= 0) {
            Py_XDECREF(op->ob_item[i]);
        }
        //釋放內存
        PyMem_FREE(op->ob_item);
    }
    //緩衝池機制
    if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
        free_list[numfree++] = op;
    else
        Py_TYPE(op)->tp_free((PyObject *)op);
    Py_TRASHCAN_SAFE_END(op)
}

咱們知道調用list3的list_clear,減小內部元素引用計數的時候,致使list4引用計數爲0。而一旦list4的引用計數爲0,那麼是否是也要執行和list3同樣的list_clear動做呢?而後會發現list3的引用計數也爲0了,所以list3也會被銷燬。循環引用,彼此共生,銷燬之路,怎能獨自前行?最終list3和list4都會執行內部的list_dealloc,釋放內部元素,調整參數,固然還有所謂的緩衝池機制等等。總之如此一來,list3和list4就都被安全地回收了。

17.4.4.5 總結

雖然有不少對象掛在垃圾收集機制監控的鏈表上,可是不少時候是引用計數機制在維護這些對象,只有引用計數無能爲力的循環引用,垃圾收集機制纔會起到做用(這裏沒有把引用計數機制當作垃圾回收,固然若是別人問你python的垃圾回收機制的時候,你也能夠把引用計數機制加上)。事實上,若是不是循環引用的話,那麼垃圾回收是無能爲力的,由於掛在垃圾回收機制上的對象都是引用計數不爲0的,若是爲0早被引用計數機制幹掉了。而引用計數不爲0的狀況只有兩種:一種是被程序使用的對象,二是循環引用中的對象。被程序使用的對象是不能被回收的,因此垃圾回收只能處理那些循環引用的對象。

因此python的垃圾回收就是:引用計數爲主,分代回收爲輔,二者結合使用,後者主要是爲了彌補前者的缺點而存在的。

17.5 python中的gc模塊

這個gc模塊,底層就是gcmodule,咱們說這些模塊底層是用c寫的,當python編譯好時,就內嵌在解釋器裏面了。咱們能夠導入它,可是在python安裝目錄上看不到。

gc.enable():開啓垃圾回收

這個函數表示開啓垃圾回收機制,默認是自動開啓的。

gc.disable():關閉垃圾回收

import gc


class A:
    pass


# 關掉gc
gc.disable()


while True:
    a1 = A()
    a2 = A()
    # 此時內部出現了循環引用
    a1.__dict__["attr"] = a2
    a2.__dict__["attr"] = a1

    # 因爲循環引用,此時是del a1, a2,光靠引用計數是刪不掉的
    # 須要垃圾回收,可是咱們給關閉了
    del a1, a2

無限循環,而且每次循環都會建立新的對象,最終致使內存無限增大。

import gc


class A:
    pass


# 關掉gc
gc.disable()


while True:
    a1 = A()
    a2 = A()

這裏即便咱們關閉了gc,可是每一次循環都會指向一個新的對象,而以前的對象因爲沒有人指向了,那麼引用計數爲0,直接就被引用計數機制幹掉了,內存會一直穩定,不會出現增加。因此咱們看到,即便關閉了gc,可是對於那些引用計數爲0的,該刪除仍是會刪除的。因此引用計數很簡單,就是按照對應的規則該加1加1,該減1減1,一旦爲0直接銷燬。而當出現循環引用的時候,才須要gc閃亮登場。這裏關閉了gc,可是沒有循環引用因此沒事,而上一個例子,關閉了gc,可是出現了循環引用,而引用計數機制只會根據引用計數來判斷,而發現引用計數不爲0,因此就一直傻傻地不回收,程序又一直建立新的對象,最終致使內存越用越多。而上一個例子如果開啓了gc,那麼分代回收計數,就會經過標記--清除的方式將產生循環引用的對象的引用計數減1,而引用計數機制發現引用計數爲0了,那麼就會將對象回收掉。因此這個引用計數機制到底算不算垃圾回收機制的一種呢?你要說算吧,我把gc關閉了,引用計數機制還能夠發揮做用,你要說不算吧,它確實是負責斷定對象是否應該被回收的惟一標準,因此該怎麼說就具體看狀況吧。

gc.isenabled():判斷gc是否開啓

import gc


print(gc.isenabled())  # True
gc.disable() 
print(gc.isenabled())  # False

gc.collect():馬上觸發垃圾回收

咱們說,垃圾回收觸發是須要條件的,好比0代鏈表,清理零代鏈表的時候,須要對象的個數count大於閾值threshold(默認是700),可是這個函數能夠強制觸發垃圾回收。

gc.get_threshold():返回每一代的閾值

import gc


print(gc.get_threshold())  # (700, 10, 10)
# 700:零代鏈表的對象超過700個,觸發垃圾回收
# 10:零代鏈表,垃圾回收10次,會清理一代鏈表
# 10:一代鏈表,垃圾回收10次,會清理二代鏈表

gc.set_threshold():設置每一代的閾值

import gc


gc.set_threshold(1000, 100, 100)
print(gc.get_threshold())  # (1000, 100, 100)

gc.get_count():查看每一代的值達到了多少

import gc


print(gc.get_count())  # (44, 7, 5)

gc.get_stats():返回每一代的具體信息

from pprint import pprint
import gc


pprint(gc.get_stats())
"""
[{'collected': 316, 'collections': 62, 'uncollectable': 0},
 {'collected': 538, 'collections': 5, 'uncollectable': 0},
 {'collected': 0, 'collections': 0, 'uncollectable': 0}]
"""

gc.get_objects():返回被垃圾回收器追蹤的全部對象,一個列表

gc.is_tracked(obj):查看對象obj是否被垃圾收集器追蹤

import gc


a = 1
b = []

print(gc.is_tracked(a))  # False
print(gc.is_tracked(b))  # True

# 咱們說只有那些可能會產生循環引用的對象纔會被垃圾回收器跟蹤

gc.get_referrers(obj):返回全部引用了obj的對象

gc.get_referents(obj):返回全部被obj引用了的對象

gc.freeze():凍結全部被垃圾回收器跟蹤的對象並在之後的垃圾回收中不被處理

gc.unfreeze():取消全部凍結的對象,讓它們繼續參數垃圾回收

gc.get_freeze_count():獲取凍結的對象個數

import gc


# 不須要參數,會自動找到被垃圾回收器跟蹤的對象
gc.freeze()
# 說明有不少內置對象在被跟蹤,被咱們凍結了
print(gc.get_freeze_count())  # 24397

b = []
gc.freeze()
# 只要這裏比上面多1個就行
print(gc.get_freeze_count())  # 24398

# 取消凍結
gc.unfreeze()
print(gc.get_freeze_count())  # 0

gc.get_debug():獲取debug級別

import gc


print(gc.get_debug())  # 0

gc.set_debug():設置debug級別

import gc


"""
DEBUG_STATS - 在垃圾收集過程當中打印全部統計信息
DEBUG_COLLECTABLE - 打印發現的可收集對象
DEBUG_UNCOLLECTABLE - 打印unreachable對象(除了uncollectable對象)
DEBUG_SAVEALL - 將對象保存到gc.garbage(一個列表)裏面,而不是釋放它
DEBUG_LEAK - 對內存泄漏的程序進行debug (everything but STATS).
    
"""
class A:
    pass


class B:
    pass


a = A()
b = B()

gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_SAVEALL)
print(gc.garbage)  # []
a.b = b
b.a = a
del a, b
gc.collect()  # 強制觸發垃圾回收
# 下面都是自動打印的
"""
gc: collecting generation 2...
gc: objects in each generation: 123 3732 20563
gc: objects in permanent generation: 0
gc: done, 4 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 24249
gc: objects in permanent generation: 0
gc: done, 0 unreachable, 0 uncollectable, 0.0150s elapsed
gc: collecting generation 2...
gc: objects in each generation: 525 0 23752
gc: objects in permanent generation: 0
gc: done, 7062 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 21941
gc: objects in permanent generation: 0
gc: done, 4572 unreachable, 0 uncollectable, 0.0000s elapsed
"""
print(gc.garbage)
# [<__main__.A object at 0x0000020CFDB50250>, <__main__.B object at 0x0000020CFDB50340>, {'b': <__main__.B object at 0x0000020CFDB50340>}, {'a': <__main__.A object at 0x0000020CFDB50250>}]

17.6 總結

儘管python採用了最經典的(最土的)的引用計數來做爲自動內存管理的方案,可是python採用了多種方式來彌補引用計數的不足,內存池的大量使用,標記--清除(分代技術採用的去除循環引用的引用計數的方式)垃圾收集技術都極大地完善了python的內存管理(包括申請、回收)機制。儘管引用計數機制須要花費額外的開銷來維護引用計數,可是如今這個年代,這點內存算個啥。並且引用計數也有好處,否則早就隨着時代的前進而被掃進歷史的垃圾堆裏面了。首先引用計數真的很方便,很直觀,對於不少對象引用計數可以直接解決,不須要什麼複雜的操做;另外引用計數將垃圾回收的開銷分攤在了整個運行時,這對於python的響應是有好處的。

固然內存管理和垃圾回收是一門給常精細和繁瑣的技術,有興趣的話各位能夠本身大刀闊斧的衝進python的源碼中自由翱翔。

相關文章
相關標籤/搜索