日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > python >内容正文

python

python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收...

發(fā)布時(shí)間:2024/1/23 python 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收... 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

17.0 序

內(nèi)存管理,對(duì)于python這樣的動(dòng)態(tài)語言是至關(guān)重要的一部分,它在很大程度上決定了python的執(zhí)行效率,因?yàn)樵趐ython的運(yùn)行中會(huì)創(chuàng)建和銷毀大量的對(duì)象,這些都設(shè)計(jì)內(nèi)存的管理。同理python還提供了了內(nèi)存的垃圾回收(GC,garbage collection),將開發(fā)者從繁瑣的手動(dòng)維護(hù)內(nèi)存的工作中解放出來。這一章我們就來分析python的GC是如何實(shí)現(xiàn)的。

17.1 內(nèi)存管理架構(gòu)

在python中內(nèi)存管理機(jī)制是分層次的,我們可以看成有四層,0 1 2 3。在最底層,也就是第0層是由操作系統(tǒng)提供的內(nèi)存管理接口,比如C提供了malloc和free接口,這一層是由操作系統(tǒng)實(shí)現(xiàn)并且管理的,python不能干涉這一行為。從這一層往上,剩余的三層則都是由python實(shí)現(xiàn)并維護(hù)的。

第一層是python基于第0層操作系統(tǒng)管理接口包裝而成的,這一層并沒有在第0層上加入太多的動(dòng)作,其目的僅僅是為python提供一層統(tǒng)一的raw memory的管理接口。這么做的原因就是雖然不同的操作系統(tǒng)都提供了ANSI C標(biāo)準(zhǔn)所定義的內(nèi)存管理接口,但是對(duì)于某些特殊情況不同操作系統(tǒng)有不同的行為。比如調(diào)用malloc(0),有的操作系統(tǒng)會(huì)返回NULL,表示申請(qǐng)失敗,但是有的操作系統(tǒng)則會(huì)返回一個(gè)貌似正常的指針, 但是這個(gè)指針指向的內(nèi)存并不是有效的。為了最廣泛的可移植性,python必須保證相同的語義一定代表著相同的運(yùn)行時(shí)行為,為了處理這些與平臺(tái)相關(guān)的內(nèi)存分配行為,python必須要在C的內(nèi)存分配接口之上再提供一層包裝。

在python中,第一層的實(shí)現(xiàn)就是一組以PyMem_為前綴的函數(shù)族,下面來看一下。

//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的語義。并且我們發(fā)現(xiàn),比如PyMem_Malloc,如果申請(qǐng)的內(nèi)存大小超過了PY_SSIZE_T_MAX直接返回NULL,并且調(diào)用了_PyMem.malloc,這個(gè)C中的malloc幾乎沒啥區(qū)別,但是會(huì)對(duì)特殊值進(jìn)行一些處理。到目前為止,僅僅是分配了raw memory而已。其實(shí)在第一層,python還提供了面向?qū)ο笾蓄愋偷膬?nèi)存分配器。

//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中需要程序員自行提供所申請(qǐng)的空間大小。然而在PyMem_New中,只需要提供類型和數(shù)量,python會(huì)自動(dòng)偵測(cè)其所需的內(nèi)存空間大小。

第一層所提供的內(nèi)存管理接口的功能是非常有限的,如果創(chuàng)建一個(gè)PyLongObject對(duì)象,還需要做很多額外的工作,比如設(shè)置對(duì)象的類型參數(shù)、初始化對(duì)象的引用計(jì)數(shù)值等等。因此為了簡(jiǎn)化python自身的開發(fā),python在比第一層更高的抽象層次上提供了第二層內(nèi)存管理接口。在這一層,是一組以PyObject_為前綴的函數(shù)族,主要提供了創(chuàng)建python對(duì)象的接口。這一套函數(shù)族又被換做Pymalloc機(jī)制。因此在第二層的內(nèi)存管理機(jī)制上,python對(duì)于一些內(nèi)建對(duì)象構(gòu)建了更高抽象層次的內(nèi)存管理策略。而對(duì)于第三層的內(nèi)存管理策略,主要就是對(duì)象的緩存機(jī)制。因此:

第0層:操作系統(tǒng)負(fù)責(zé)管理內(nèi)存,python無權(quán)干預(yù)

第1層:僅僅對(duì)c中原生的malloc進(jìn)行了簡(jiǎn)單包裝

第2層:真正在python中發(fā)揮巨大作用,并且也是GC的藏身之處

第3層:緩沖池,比如小整數(shù)對(duì)象池等等。

下面我們就來對(duì)第二層內(nèi)存管理機(jī)制進(jìn)行剖析。

17.2 小塊空間的內(nèi)存池

在python中,很多時(shí)候申請(qǐng)的內(nèi)存都是小塊的內(nèi)存,這些小塊的內(nèi)存在申請(qǐng)后很快又被釋放,并且這些內(nèi)存的申請(qǐng)并不是為了創(chuàng)建對(duì)象,所以并沒有對(duì)象一級(jí)的內(nèi)存池機(jī)制。這就意味著python在運(yùn)行期間需要大量的執(zhí)行malloc和free操作,導(dǎo)致操作系統(tǒng)在用戶態(tài)和內(nèi)核態(tài)之間進(jìn)行切換,這將嚴(yán)重影響python的效率。所以為了提高執(zhí)行效率,python引入了一個(gè)內(nèi)存池機(jī)制,用于管理對(duì)小塊內(nèi)存的申請(qǐng)和釋放,這就是之前說的Pymalloc機(jī)制,并且提供了pymalloc_alloc,pymalloc_realloc,pymalloc_free三個(gè)接口。

整個(gè)小塊內(nèi)存的內(nèi)存池可以視為一個(gè)層次結(jié)構(gòu),在這個(gè)層次結(jié)構(gòu)中一共分為4層,從下至上分別是:block、pool、arena和內(nèi)存池。并且block(霧)、pool、arena都是python代碼中可以找到的實(shí)體,而最頂層的內(nèi)存池只是一個(gè)概念上的東西,表示python對(duì)整個(gè)小塊內(nèi)存分配和釋放行為的內(nèi)存管理機(jī)制。

17.2.1 block

在最底層,block是一個(gè)確定大小的內(nèi)存塊。而python中,有很多種block,不同種類的block都有不同的內(nèi)存大小,這個(gè)內(nèi)存大小的值被稱之為size?class。為了在當(dāng)前主流的32位平臺(tái)和64位平臺(tái)都能獲得最佳性能,所有的block的長(zhǎng)度都是8字節(jié)對(duì)齊的。

//obmalloc.c

#define ALIGNMENT 8 /* must be 2^N */

#define ALIGNMENT_SHIFT 3

同時(shí),python為block的大小設(shè)定了一個(gè)上限,當(dāng)申請(qǐng)的內(nèi)存大小小于這個(gè)上限時(shí),python可以使用不同種類的block滿足對(duì)內(nèi)存的需求;當(dāng)申請(qǐng)的內(nèi)存大小超過了這個(gè)上限,python就會(huì)將對(duì)內(nèi)存的請(qǐng)求轉(zhuǎn)交給第一層的內(nèi)存管理機(jī)制,即PyMem函數(shù)族來處理。這個(gè)上限值在python中被設(shè)置為512,如果超過了這個(gè)值還是要經(jīng)過操作系統(tǒng)臨時(shí)申請(qǐng)的。

//obmalloc.c

#define SMALL_REQUEST_THRESHOLD 512

#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)

根據(jù)SMALL_REQUEST_THRESHOLD和ALIGNMENT的限定,實(shí)際上我們可以由此得到不同種類的block和size class。block是以8字節(jié)對(duì)齊,那么每一個(gè)塊的大小都是8的整倍數(shù),最大不超過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

因此當(dāng)我們申請(qǐng)一個(gè)44字節(jié)的內(nèi)存時(shí),PyObject_Malloc會(huì)從內(nèi)存池中劃分一個(gè)48字節(jié)的block給我們。

另外在python中,block只是一個(gè)概念,在python源碼中沒有與之對(duì)應(yīng)的實(shí)體存在。之前我們說對(duì)象,對(duì)象在源碼中有對(duì)應(yīng)的PyObject,列表在源碼中則有對(duì)應(yīng)的PyListObject,但是這里的block僅僅是概念上的東西,我們知道它是具有一定大小的內(nèi)存,但是它并不與python源碼里面的某個(gè)東西對(duì)應(yīng)。但是,python提供了一個(gè)管理block的東西,也就是我們下面要分析的pool。

17.2.2 pool

一組block的集合成為一個(gè)pool,換句話說,一個(gè)pool管理著一堆具有固定大小的內(nèi)存塊(block)。事實(shí)上,pool管理者一大塊內(nèi)存,它有一定的策略,將這塊大的內(nèi)存劃分為多個(gè)小的內(nèi)存塊。在python中,一個(gè)pool的大小通常是為一個(gè)系統(tǒng)內(nèi)存頁(yè),也就是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提供對(duì)應(yīng)的結(jié)構(gòu),但是提供了和pool相關(guān)的結(jié)構(gòu),我們來看看

//obmalloc.c

/* Pool for small blocks. */

struct pool_header {

union { block *_padding;

uint count; } ref; /* 當(dāng)然pool里面的block數(shù)量 */

block *freeblock; /* 一個(gè)鏈表,指向下一個(gè)可用的block */

struct pool_header *nextpool; /* 指向下一個(gè)pool */

struct pool_header *prevpool; /* 指向上一個(gè)pool "" */

uint arenaindex; /* 在area里面的索引 */

uint szidx; /* block的大小(固定值?后面說) */

uint nextoffset; /* 下一個(gè)可用block的內(nèi)存偏移量 */

uint maxnextoffset; /* 最后一個(gè)block距離開始位置的距離 */

};

typedef struct pool_header *poolp;

我們剛才說了一個(gè)pool的大小在python中是4KB,但是從當(dāng)前的這個(gè)pool的結(jié)構(gòu)體來看,用鼻子想也知道吃不完4KB的內(nèi)存。所以呀,這個(gè)結(jié)構(gòu)體叫做pool_header,它僅僅一個(gè)pool的頭部,除去這個(gè)pool_header,還剩下的內(nèi)存才是維護(hù)的所有block的集合所占的內(nèi)存。

我們注意到,pool_header里面有一個(gè)szidx,這就意味著pool里面管理的內(nèi)存塊大小都是一樣的。也就是說,一個(gè)pool可能管理了20個(gè)32字節(jié)的block、也可能管理了20個(gè)64字節(jié)的block,但是不會(huì)出現(xiàn)管理了10個(gè)32字節(jié)的block加上10個(gè)64字節(jié)的block存在。每一個(gè)pool都和一個(gè)size聯(lián)系在一起,更確切的說都和一個(gè)size class index聯(lián)系在一起,表示pool里面存儲(chǔ)的block都是多少字節(jié)的。這就是里面的域szidx存在的意義。

假設(shè)我們手里有一塊4kb的內(nèi)存,來看看python是如何將這塊內(nèi)存改造為一個(gè)管理32字節(jié)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的內(nèi)存

init_pool:

pool->ref.count = 1;

...

//設(shè)置pool的size class index

pool->szidx = size;

//將size class index轉(zhuǎn)換成size,比如:0->8, 1->16, 63->512

size = INDEX2SIZE(size);

//跳過用于pool_header的內(nèi)存,并進(jìn)行對(duì)齊

bp = (block *)pool + POOL_OVERHEAD;

//等價(jià)于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已經(jīng)被分配了,所以在ref.count中記錄了當(dāng)前已經(jīng)被分配的block的數(shù)量,這時(shí)為1,特別需要注意的是,bp返回的實(shí)際上是一個(gè)地址,這個(gè)地址之后有將近4kb的內(nèi)存實(shí)際上都是可用的,但是可以肯定申請(qǐng)內(nèi)存的函數(shù)只會(huì)使用[bp, bp+size]這個(gè)區(qū)間的內(nèi)存,這是由size?class index可以保證的。改造成pool之后的4kb內(nèi)存如圖所示:

實(shí)線箭頭是指針,但是虛線箭頭則是偏移位置的形象表示。在nextoffset,maxnextoffset中存儲(chǔ)的是相對(duì)于pool頭部的偏移位置。

在了解初始化之后的pool的樣子之后,可以來看看python在申請(qǐng)block時(shí),pool_header中的各個(gè)域是怎么變動(dòng)的。假設(shè)我們從現(xiàn)在開始連續(xù)申請(qǐng)5塊28字節(jié)內(nèi)存,由于28字節(jié)對(duì)應(yīng)的size class index為3,所以實(shí)際上會(huì)申請(qǐng)5塊32字節(jié)的內(nèi)存。

//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數(shù)自增1

++pool->ref.count;

//這里的freeblock指向的是下一個(gè)可用的block的起始地址

bp = pool->freeblock;

assert(bp != NULL);

if ((pool->freeblock = *(block **)bp) != NULL) {

goto success;

}

//因此當(dāng)再次申請(qǐng)32字節(jié)block時(shí),只需要返回freeblock指向的地址就可以了。那么很顯然,freeblock需要前進(jìn),指向下一個(gè)可用的block,這個(gè)時(shí)候nextoffset就現(xiàn)身了

if (pool->nextoffset <= pool->maxnextoffset) {

//當(dāng)nextoffset小于等于maxoffset時(shí)候

//freeblock等于當(dāng)前block的地址 + nextoffset(下一個(gè)可用block的內(nèi)存偏移量)

//所以freeblock正好指向了下一個(gè)可用block的地址

pool->freeblock = (block*)pool +

pool->nextoffset;

//同理,nextoffset也要向前移動(dòng)一個(gè)block的距離

pool->nextoffset += INDEX2SIZE(size);

//依次反復(fù),即可對(duì)所有的block進(jìn)行遍歷。而maxnextoffset指明了該pool中最后一個(gè)可用的block距離pool開始位置的偏移

//當(dāng)pool->nextoffset > pool->maxnextoffset就意味著遍歷完pool中的所有block了

//再次獲取顯然就是NULL了

*(block **)(pool->freeblock) = NULL;

goto success;

}

}

所以,申請(qǐng)、前進(jìn)、申請(qǐng)、前進(jìn),一直重復(fù)著相同的動(dòng)作,整個(gè)過程非常自然,也容易理解。但是我們發(fā)現(xiàn),由于無論多少個(gè)block,這些block必須都是具有相同大小,導(dǎo)致一個(gè)pool中只能滿足POOL_SIZE?/?size次對(duì)block的申請(qǐng),這就讓人不舒服。舉個(gè)栗子,現(xiàn)在我們已經(jīng)進(jìn)行了5次連續(xù)32字節(jié)的內(nèi)存分配,可以想象,pool中5個(gè)連續(xù)的block都被分配出去了。過了一段時(shí)間,程序釋放了其中的第2塊和第4塊block,那么下一次再分配32字節(jié)的內(nèi)存的時(shí)候,pool提交的應(yīng)該是第2塊,還是第6塊呢?顯然為了pool的使用效率,最好分配自由的第二塊block。因此可以想象,一旦python運(yùn)轉(zhuǎn)起來,內(nèi)存的釋放動(dòng)作將導(dǎo)致pool中出現(xiàn)大量的離散的自由block,python為了知道哪些block是被使用之后再次被釋放的,必須建立一種機(jī)制,將這些離散自由的block組合起來,再次使用。這個(gè)機(jī)制就是所有的自由block鏈表,這個(gè)鏈表的關(guān)鍵就在pool_header中的那個(gè)freeblock身上。

//obmalloc.c

/* Pool for small blocks. */

struct pool_header {

union { block *_padding;

uint count; } ref; /* 當(dāng)然pool里面的block數(shù)量 */

block *freeblock; /* 一個(gè)鏈表,指向下一個(gè)可用的block */

struct pool_header *nextpool; /* 指向下一個(gè)pool */

struct pool_header *prevpool; /* 指向上一個(gè)pool "" */

uint arenaindex; /* 在area里面的索引 */

uint szidx; /* block的大小(固定值?后面說) */

uint nextoffset; /* 下一個(gè)可用block的內(nèi)存偏移量 */

uint maxnextoffset; /* 最后一個(gè)block距離開始位置的距離 */

};

typedef struct pool_header *poolp;

剛才我們說了,當(dāng)pool初始化完后之后,freeblock指向了一個(gè)有效的地址,也就是下一個(gè)可以分配出去的block的地址。然而奇特的是,當(dāng)python設(shè)置了freeblock時(shí),還設(shè)置了*freeblock。這個(gè)動(dòng)作看似詭異,然而我們馬上就能看到設(shè)置*freeblock的動(dòng)作正是建立離散自由block鏈表的關(guān)鍵所在。目前我們看到的freeblock只是在機(jī)械地前進(jìn)前進(jìn),因?yàn)樗诘却粋€(gè)特殊的時(shí)刻,在這個(gè)特殊的時(shí)刻,你會(huì)發(fā)現(xiàn)freeblock開始成為一個(gè)蘇醒的精靈,在這4kb的內(nèi)存上開始靈活地舞動(dòng)。這個(gè)特殊的時(shí)刻就是一個(gè)block被釋放的時(shí)刻。

//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就是勢(shì)必大于0

assert(pool->ref.count > 0); /* else it was empty */

*(block **)p = lastfree = pool->freeblock;

pool->freeblock = (block *)p;

}

在釋放block時(shí),神秘的freeblock驚鴻一現(xiàn),覆蓋在freeblock身上的那層面紗就要被揭開了。我們知道,這是freeblock雖然指向了一個(gè)有效的pool里面的地址,但是*freeblock是為NULL的。假設(shè)這時(shí)候python釋放的是block A,那么A中的第一個(gè)字節(jié)的值被設(shè)置成了當(dāng)前freeblock的值,然后freeblock的值被更新了,指向了block A的首地址。就是這兩個(gè)步驟,一個(gè)block被插入到了離散自由的block鏈表中,所以當(dāng)?shù)?塊和第4塊block都被釋放之后,我們可以看到一個(gè)初具規(guī)模的離散自由block鏈表了。

到了這里,這條實(shí)現(xiàn)方式非常奇特的block鏈表被我們挖掘出來了,從freeblock開始,我們可以很容易的以freeblock?=?*freeblock的方式遍歷這條鏈表,而當(dāng)發(fā)現(xiàn)了*freeblock為NULL時(shí),則表明到達(dá)了該鏈表(可用自由鏈表)的尾部了,那么下次就需要申請(qǐng)新的block了。

//obmalloc.c

static int

pymalloc_alloc(void *ctx, void *p)

{

if (pool != pool->nextpool) {

++pool->ref.count;

bp = pool->freeblock;

assert(bp != NULL);

//如果這里的條件不為真,表明離散自由鏈表中已經(jīng)不存在可用的block了

//如果可能,則會(huì)繼續(xù)分配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這個(gè)條件都不成立了呢?pool的大小有限制啊,如果我再想申請(qǐng)block的時(shí)候,沒空間了怎么辦?再來一個(gè)pool不就好了,所以多個(gè)block可以組合成一個(gè)集合,pool;那么多個(gè)pool也可以組合起來,就是我們下面介紹的arena。

17.2.3 arena

在python中,多個(gè)pool聚合的結(jié)果就是一個(gè)arena。上一節(jié)提到,pool的大小默認(rèn)是4kb,同樣每個(gè)arena的大小也有一個(gè)默認(rèn)值。#define ARENA_SIZE (256 << 10),顯然這個(gè)值默認(rèn)是256KB,也就是ARENA_SIZE?/?POOL_SIZE?=?64個(gè)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;

};

一個(gè)概念上的arena在python源碼中就對(duì)應(yīng)arena_object結(jié)構(gòu)體,確切的說,arena_object僅僅是arena的一部分。就像pool_header僅僅是pool的一部分一樣,一個(gè)完整的pool包括一個(gè)pool_header和透過這個(gè)pool_header管理著的block集合;一個(gè)完整的arena也包括一個(gè)arena_object和透過這個(gè)arena_object管理著的pool集合。

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

在arena_object結(jié)構(gòu)體的定義中,我們看到了nextarena和prevarena這兩個(gè)東西,這似乎意味著在python中會(huì)有一個(gè)或多個(gè)arena構(gòu)成的鏈表,這個(gè)鏈表的表頭就是arenas。呃,這種猜測(cè)實(shí)際上只對(duì)了一半,實(shí)際上,在python中確實(shí)會(huì)存在多個(gè)arena_object構(gòu)成的集合,但是這個(gè)集合不夠成鏈表,而是一個(gè)數(shù)組。數(shù)組的首地址由arenas來維護(hù),這個(gè)數(shù)組就是python中的通用小塊內(nèi)存的內(nèi)存池。另一方面,nextarea和prevarena也確實(shí)是用來連接arena_object組成鏈表的,咦,不是已經(jīng)構(gòu)成或數(shù)組了嗎?為啥又要來一個(gè)鏈表。

我們?cè)farena是用來管理一組pool的集合的,arena_object的作用看上去和pool_header的作用是一樣的。但是實(shí)際上,pool_header管理的內(nèi)存和arena_object管理的內(nèi)存有一點(diǎn)細(xì)微的差別。pool_header管理的內(nèi)存pool_header自身是一塊連續(xù)的內(nèi)存,但是arena_object與其管理的內(nèi)存則是分離的:

咋一看,貌似沒啥區(qū)別,不過一個(gè)是連著的,一個(gè)是分開的。但是這后面隱藏了這樣一個(gè)事實(shí):當(dāng)pool_header被申請(qǐng)時(shí),它所管理的內(nèi)存也一定被申請(qǐng)了;但是當(dāng)arena_object被申請(qǐng)時(shí),它所管理的pool集合的內(nèi)存則沒有被申請(qǐng)。換句話說,arena_object和pool集合在某一時(shí)刻需要建立聯(lián)系。

當(dāng)一個(gè)arena的arena_object沒有與pool建立聯(lián)系的時(shí)候,這時(shí)的arena就處于"未使用"狀態(tài);一旦建立了聯(lián)系,這時(shí)arena就轉(zhuǎn)換到了"可用"狀態(tài)。對(duì)于每一種狀態(tài),都有一個(gè)arena鏈表。"未使用"的arena鏈表表頭是unused_arena_objects,多個(gè)arena之間通過nextarena連接,并且是一個(gè)單向的鏈表;而"可用的"arena鏈表表頭是usable_arenas,多個(gè)arena之間通過nextarena、prevarena連接,是一個(gè)雙向鏈表。

申請(qǐng)arena

在運(yùn)行期間,python使用new_arena來創(chuàng)建一個(gè)arena,我們來看看它是如何被創(chuàng)建的。

//obmalloc.c

//arenas,多個(gè)arena組成的數(shù)組的首地址

static struct arena_object* arenas = NULL;

//當(dāng)arena數(shù)組中的所有arena的個(gè)數(shù)

static uint maxarenas = 0;

//未使用的arena的個(gè)數(shù)

static struct arena_object* unused_arena_objects = NULL;

//可用的arena的個(gè)數(shù)

static struct arena_object* usable_arenas = NULL;

//初始化需要申請(qǐng)的arena的個(gè)數(shù)

#define INITIAL_ARENA_OBJECTS 16

static struct arena_object*

new_arena(void)

{

//arena,一個(gè)arena_object結(jié)構(gòu)體對(duì)象

struct arena_object* arenaobj;

uint excess; /* number of bytes above pool alignment */

//[1]:判斷是否需要擴(kuò)充"未使用"的arena列表

if (unused_arena_objects == NULL) {

uint i;

uint numarenas;

size_t nbytes;

//[2]:確定本次需要申請(qǐng)的arena_object的個(gè)數(shù),并申請(qǐng)內(nèi)存

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]:初始化新申請(qǐng)的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鏈表中取出一個(gè)"未使用"的arena

assert(unused_arena_objects != NULL);

arenaobj = unused_arena_objects;

unused_arena_objects = arenaobj->nextarena;

assert(arenaobj->address == 0);

//[5]:申請(qǐng)arena管理的內(nèi)存,這里我們說的arena指的是arena_object,簡(jiǎn)寫了

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;

//調(diào)整個(gè)數(shù)

++narenas_currently_allocated;

++ntimes_arena_allocated;

if (narenas_currently_allocated > narenas_highwater)

narenas_highwater = narenas_currently_allocated;

//[6]:設(shè)置poo集合的相關(guān)信息,這是設(shè)置為NULL

arenaobj->freepools = NULL;

/* pool_address

nfreepools

arenaobj->pool_address = (block*)arenaobj->address;

arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;

assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);

//將pool的起始地址調(diào)整為系統(tǒng)頁(yè)的邊界

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首先會(huì)檢查當(dāng)前"未使用"鏈表中是否還有"未使用"arena,檢查的結(jié)果將決定后續(xù)的動(dòng)作。

如果在"未使用"鏈表中還存在未使用的arena,那么python會(huì)從"未使用"arena鏈表中抽取一個(gè)arena,接著調(diào)整"未使用"鏈表,讓它和抽取的arena斷絕一切聯(lián)系。然后python申請(qǐng)了一塊256KB大小的內(nèi)存,將申請(qǐng)的內(nèi)存地址賦給抽取出來的arena的address。我們已經(jīng)知道,arena中維護(hù)的是pool集合,這塊256KB的內(nèi)存就是pool的容身之處,這時(shí)候arena就已經(jīng)和pool集合建立聯(lián)系了。這個(gè)arena已經(jīng)具備了成為"可用"內(nèi)存的條件,該arena和"未使用"arena鏈表脫離了關(guān)系,就等著被"可用"arena鏈表接收了,不過什么時(shí)候接收呢?先別急

隨后,python在代碼的[6]處設(shè)置了一些arena用戶維護(hù)pool集合的信息。需要注意的是,python將申請(qǐng)到的256KB內(nèi)存進(jìn)行了處理,主要是放棄了一些內(nèi)存,并將可使用的內(nèi)存邊界(pool_address)調(diào)整到了與系統(tǒng)頁(yè)對(duì)齊。然后通過arenaobj->freepools = NULL;將freepools設(shè)置為NULL,這不奇怪,基于對(duì)freeblock的了解,我們知道要等到釋放一個(gè)pool時(shí),這個(gè)freepools才會(huì)有用。最后我們看到,pool集合占用的256KB內(nèi)存在進(jìn)行邊界對(duì)齊后,實(shí)際是交給pool_address來維護(hù)了。

回到new_arena中的[1]處,如果unused_arena_objects為NULL,則表明目前系統(tǒng)中已經(jīng)沒有"未使用"arena了,那么python首先會(huì)擴(kuò)大系統(tǒng)的arena集合(小塊內(nèi)存內(nèi)存池)。python在內(nèi)部通過一個(gè)maxarenas的變量維護(hù)了存儲(chǔ)arena的數(shù)組的個(gè)數(shù),然后在[2]處將待申請(qǐng)的arena的個(gè)數(shù)設(shè)置為當(dāng)然arena個(gè)數(shù)(maxarenas)的2倍。當(dāng)然首次初始化的時(shí)候maxarenas為0,此時(shí)為16。

在獲得了新的maxarenas后,python會(huì)檢查這個(gè)新得到的值是否溢出了。如果檢查順利通過,python就會(huì)在[3]處通過realloc擴(kuò)大arenas指向的內(nèi)存,并對(duì)新申請(qǐng)的arena_object進(jìn)行設(shè)置,特別是那個(gè)不起眼的address,要將新申請(qǐng)的address一律設(shè)置為0。實(shí)際上,這是一個(gè)標(biāo)識(shí)arena是出于"未使用"狀態(tài)還是"可用"狀態(tài)的重要標(biāo)記。而一旦arena(arena_object)和pool集合建立了聯(lián)系,這個(gè)address就變成了非0,看代碼的[6]處。當(dāng)然別忘記我們?yōu)槭裁磿?huì)走到[3]這里,是因?yàn)閡nused_arena_objects == NULL了,而且最后還設(shè)置了unused_arena_objects,這樣系統(tǒng)中又有了"未使用"的arena了,接下來python就在[4]處對(duì)一個(gè)arena進(jìn)行初始化了。

17.2.4 內(nèi)存池

可用pool緩沖池--usedpools

通過#define SMALL_REQUEST_THRESHOLD 512我們知道python內(nèi)部默認(rèn)的小塊內(nèi)存與大塊內(nèi)存的分界點(diǎn)定在512個(gè)字節(jié)。也就是說,當(dāng)申請(qǐng)的內(nèi)存小于512個(gè)字節(jié),pymalloc_alloc會(huì)在內(nèi)存池中申請(qǐng)內(nèi)存,而當(dāng)申請(qǐng)的內(nèi)存超過了512字節(jié),那么pymalloc_alloc將退化為malloc,通過操作系統(tǒng)來申請(qǐng)內(nèi)存。當(dāng)然,通過修改python源代碼我們可以改變這個(gè)值,從而改變python的默認(rèn)內(nèi)存管理行為。

當(dāng)申請(qǐng)的內(nèi)存小于512字節(jié)時(shí),python會(huì)使用area所維護(hù)的內(nèi)存空間。那么python內(nèi)部對(duì)于area的個(gè)數(shù)是否有限制呢?換句話說,python對(duì)于這個(gè)小塊空間內(nèi)存池的大小是否有限制?其實(shí)這個(gè)決策取決于用戶,python提供了一個(gè)編譯符號(hào),用于控制是否限制內(nèi)存池的大小,不過這里不是重點(diǎn),只需要知道就行。

盡管我們?cè)谇懊婊瞬簧倨榻Barena,同時(shí)也看到arena是python的小塊內(nèi)存池的最上層結(jié)構(gòu),所有arena的集合實(shí)際就是小塊內(nèi)存池。然而在實(shí)際的使用中,python并不直接與arenas和arena數(shù)組打交道。當(dāng)python申請(qǐng)內(nèi)存時(shí),最基本的操作單元并不是arena,而是pool。估計(jì)到這里懵了,別急,慢慢來。

舉個(gè)例子,當(dāng)我們申請(qǐng)一個(gè)28字節(jié)的內(nèi)存時(shí),python內(nèi)部會(huì)在內(nèi)存池尋找一塊能夠滿足需求的pool,從中取出一個(gè)block返回,而不會(huì)去尋找arena。這實(shí)際上是由pool和arena的屬性決定的,在python中,pool是一個(gè)有size概念的內(nèi)存管理抽象體,一個(gè)pool中的block總是有確定的大小,這個(gè)pool總是和某個(gè)size class index對(duì)應(yīng),還記得pool_header中的那個(gè)szidx么?而arena是沒有size概念的內(nèi)存管理抽象體。這就意味著,同一個(gè)arena在某個(gè)時(shí)刻,其內(nèi)部的pool集合可能都是32字節(jié)的block;而到了另一個(gè)時(shí)刻,由于系統(tǒng)需要,這個(gè)arena可能被重新劃分,其中的pool集合可能改為64字節(jié)的block了,甚至pool集合中一般的pool管理32字節(jié),另一半管理64字節(jié)。這就決定了在進(jìn)行內(nèi)存分配和銷毀時(shí),所有的動(dòng)作都是在pool上完成的。

當(dāng)然內(nèi)存池中的pool不僅僅是一個(gè)有size概念的內(nèi)存管理抽象體,更進(jìn)一步的,它還是一個(gè)有狀態(tài)的內(nèi)存管理抽象體。一個(gè)pool在python運(yùn)行的任何一個(gè)時(shí)刻,總是處于一下三種狀態(tài)中的一種:

used狀態(tài):pool中至少有一個(gè)block已經(jīng)被使用,并且至少有一個(gè)block未被使用。這種狀態(tài)的pool受控于python內(nèi)部維護(hù)的usedpools數(shù)組。

full狀態(tài):pool中所有的block都已經(jīng)被使用,這種狀態(tài)的pool在arena中,但是不再arena的freepools鏈表中。

empty狀態(tài):pool中所有的block都未被使用,處于這個(gè)狀態(tài)的pool的集合通過其pool_header中的nextpool構(gòu)成一個(gè)鏈表,這個(gè)鏈表的表頭就是arena中的freepools。

請(qǐng)注意:arena中處于full狀態(tài)的pool是各自獨(dú)立,沒有像其他狀態(tài)的pool一樣,連接成一個(gè)鏈表。

我們從圖中看到所有的處于used狀態(tài)的pool都被置于usedpools的控制之下。python內(nèi)部維護(hù)的usedpools數(shù)組是一個(gè)非常巧妙的實(shí)現(xiàn),維護(hù)著所有的處于used狀態(tài)的pool。當(dāng)申請(qǐng)內(nèi)存時(shí),python就會(huì)通過usedpools尋找到一個(gè)可用的pool(處于used狀態(tài)),從中分配一個(gè)block。因此我們想,一定有一個(gè)usedpools相關(guān)聯(lián)的機(jī)制,完成從申請(qǐng)的內(nèi)存的大小到size class index之間的轉(zhuǎn)換,否則python就無法找到最合適的pool了。這種機(jī)制和usedpools的結(jié)構(gòu)有著密切的關(guān)系,我們看一下它的結(jié)構(gòu)。

//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之前好像出現(xiàn)過,但是不用說也知道這表示當(dāng)前配置下有多少個(gè)不同size的塊

//在我當(dāng)前的機(jī)器就是512/8=64個(gè),對(duì)應(yīng)的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 */

};

感覺這個(gè)數(shù)組有點(diǎn)怪異,別急我們來畫圖看一看

考慮一下當(dāng)申請(qǐng)28字節(jié)的情形,前面我們說到,python首先會(huì)獲得size class index,顯然這里是3。那么在usedpools中,尋找第3+3=6個(gè)元素,發(fā)現(xiàn)usedpools[6]的值是指向usedpools[4]的地址。好暈啊,好吧,現(xiàn)在對(duì)照pool_header的定義來看一看usedpools[6]?->?nextpool這個(gè)指針指向哪里了呢?

//obmalloc.c

/* Pool for small blocks. */

struct pool_header {

union { block *_padding;

uint count; } ref; /* 當(dāng)然pool里面的block數(shù)量 */

block *freeblock; /* 一個(gè)鏈表,指向下一個(gè)可用的block */

struct pool_header *nextpool; /* 指向下一個(gè)pool */

struct pool_header *prevpool; /* 指向上一個(gè)pool "" */

uint arenaindex; /* 在area里面的索引 */

uint szidx; /* block的大小(固定值?后面說) */

uint nextoffset; /* 下一個(gè)可用block的內(nèi)存偏移量 */

uint maxnextoffset; /* 最后一個(gè)block距離開始位置的距離 */

};

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

想象一下,當(dāng)我們手中有一個(gè)size class為32字節(jié)的pool,想要將其放入這個(gè)usedpools中時(shí),要怎么做呢?從上面的描述我們知道,只需要進(jìn)行usedpools[i+i]?->?nextpool?=?pool即可,其中i為size class index,對(duì)應(yīng)于32字節(jié),這個(gè)i為3.當(dāng)下次需要訪問size class 為32字節(jié)(size class index為3)的pool時(shí),只需要簡(jiǎn)單地訪問usedpools[3+3]就可以得到了。python正是使用這個(gè)usedpools快速地從眾多的pool中快速地尋找到一個(gè)最適合當(dāng)前內(nèi)存需求的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狀態(tài)的pool

}

Pool的初始化

當(dāng)python啟動(dòng)之后,在usedpools這個(gè)小塊空間的內(nèi)存池中,并不存在任何可用的內(nèi)存,準(zhǔn)確的說,不存在任何可用的pool。在這里,python采用了延遲分配的策略,即當(dāng)我們確實(shí)開始申請(qǐng)小塊內(nèi)存的時(shí)候,python才建立這個(gè)內(nèi)存池。正如之前提到的,當(dāng)我們開始申請(qǐng)28字節(jié)的內(nèi)存時(shí),python實(shí)際將申請(qǐng)32字節(jié)的內(nèi)存,然后會(huì)首先根據(jù)32字節(jié)對(duì)應(yīng)的class size index(3)在usedpools中對(duì)應(yīng)的位置查找,如果發(fā)現(xiàn)在對(duì)應(yīng)的位置后面沒有連接任何可用的pool,python會(huì)從"可用"arena鏈表中的第一個(gè)可用的arena中獲取的一個(gè)pool。不過需要注意的是,當(dāng)前獲得的arena中包含的這些pools中可能會(huì)具有不同的class size index。

想象一下,當(dāng)申請(qǐng)32字節(jié)的內(nèi)存時(shí),從"可用"arena中取出一個(gè)pool用作32字節(jié)的pool。當(dāng)下一次內(nèi)存分配請(qǐng)求分配64字節(jié)的內(nèi)存時(shí),python可以直接使用當(dāng)前"可用"的arena的另一個(gè)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狀態(tài)的pool

if (usable_arenas == NULL) {

//嘗試申請(qǐng)新的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鏈表中第一個(gè)arena的freepools中抽取一個(gè)可用的pool

pool = usable_arenas->freepools;

if (pool != NULL) {

/* Unlink from cached pools. */

usable_arenas->freepools = pool->nextpool;

//調(diào)整可用arena鏈表中第一個(gè)arena中的可用pool的數(shù)量

--usable_arenas->nfreepools;

//如果調(diào)整之后變?yōu)?,則將該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:

...

}

可以看到,如果開始時(shí)"可用"arena鏈表為空,那么python會(huì)通過new_arena申請(qǐng)一個(gè)arena,開始構(gòu)建"可用"arena鏈表。還記得我們之前遺留了一個(gè)問題嗎?答案就在這里。在這里,一個(gè)脫離了"未使用"arena鏈表并轉(zhuǎn)變?yōu)?#34;可用"的arena被納入了"可用"arena鏈表的控制。所以python會(huì)嘗試從"可用"arena鏈表中的第一個(gè)arena所維護(hù)的pool集合中取出一個(gè)可用的pool。如果成功地取出了這個(gè)pool,那么python就會(huì)進(jìn)行一些維護(hù)信息的更新工作,甚至在當(dāng)前arena中可用的pool已經(jīng)用完了之后,將該arena從"可用"arena鏈表中移除

好了,現(xiàn)在我們手里有了一塊用于32字節(jié)內(nèi)存分配的pool,為了提高以后內(nèi)存分配的效率,我們需要將這個(gè)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結(jié)構(gòu),直接返回pool中的一個(gè)block

if (pool->szidx == size) {

bp = pool->freeblock;

assert(bp != NULL);

pool->freeblock = *(block **)bp;

goto success;

}

//pool之前就具有正確的size結(jié)果,直接返回pool中的一個(gè)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;

}

}

具體的細(xì)節(jié)可以自己觀察源代碼去研究,這里不再寫了,有點(diǎn)累

block的釋放

考察完了對(duì)block的分配,是時(shí)候來看看對(duì)block的釋放了。對(duì)block的釋放實(shí)際上就是將一塊block歸還給pool,我們已經(jīng)知道pool可能存在3種狀態(tài),在分別處于三種狀態(tài),它們的位置是各不相同的。

當(dāng)我們釋放一個(gè)block之后,可能會(huì)引起pool狀態(tài)的轉(zhuǎn)變,這種轉(zhuǎn)變可分為兩種情況

used狀態(tài)轉(zhuǎn)變?yōu)閑mpty狀態(tài)

full狀態(tài)轉(zhuǎn)變?yōu)閡sed狀態(tài)

//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 */

//設(shè)置離散自由的block鏈表

*(block **)p = lastfree = pool->freeblock;

pool->freeblock = (block *)p;

//如果!lastfree成立,那么意味著不存在lastfree,說明這個(gè)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.

*/

//當(dāng)前pool處于full狀態(tài),在釋放一塊block之后,需要將其轉(zhuǎn)換為used狀態(tài)

//并重新鏈入到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回收了一個(gè)block之后,不需要從used狀態(tài)轉(zhuǎn)換為empty狀態(tài)

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維護(hù)的鏈表中

ao = &arenas[pool->arenaindex];

pool->nextpool = ao->freepools;

ao->freepools = pool;

nf = ++ao->nfreepools;

if (nf == ao->ntotalpools) {

//調(diào)整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;

}

//調(diào)整"未使用"arena鏈表

ao->nextarena = unused_arena_objects;

unused_arena_objects = ao;

//程序走到這一步,表示是pool原先是used,釋放block之后依舊是used

//那么會(huì)將內(nèi)存歸還給操作系統(tǒng)

_PyObject_Arena.free(_PyObject_Arena.ctx,

(void *)ao->address, ARENA_SIZE);

//設(shè)置address,將arena的狀態(tài)轉(zhuǎn)為"未使用"

ao->address = 0; /* mark unassociated */

--narenas_currently_allocated;

goto success;

}

}

實(shí)際上在python2.4之前,python的arena是不會(huì)釋放pool的。這樣的話就會(huì)引起內(nèi)存泄漏,比如我們申請(qǐng)10?*?1024?*?1024個(gè)16字節(jié)的小內(nèi)存,這就意味著必須使用160MB的內(nèi)存,由于python會(huì)默認(rèn)全部使用arena(這一點(diǎn)我們沒有提)來滿足你的需求。但是當(dāng)我們將所有16字節(jié)的內(nèi)存全部釋放了,這些內(nèi)存也會(huì)回到arena的控制之中,這都沒有問題。但是問題來了,這些內(nèi)存是被arena控制的,并沒有交給操作系統(tǒng)啊,,所以這160MB的內(nèi)存始終會(huì)被python占用,如果后面程序再也不需要160MB如此巨大的內(nèi)存,那么不就浪費(fèi)了嗎?

由于這種情況必須在大量持續(xù)申請(qǐng)小內(nèi)存對(duì)象時(shí)才會(huì)出現(xiàn),因?yàn)榇蟮脑挄?huì)自動(dòng)交給操作系統(tǒng)了,小的才會(huì)由arena控制,而持續(xù)申請(qǐng)大量小內(nèi)存的情況幾乎不會(huì)碰到,所以這個(gè)問題也就留在了 Python中。但是因?yàn)橛行┤税l(fā)現(xiàn)了這個(gè)問題,所以這個(gè)問題在python2.5的時(shí)候就得到了解決。

因?yàn)樵缙诘膒ython,arena是沒有區(qū)分"未使用"和"可用"兩種狀態(tài)的,到了python2.5中,arena已經(jīng)可以將自己維護(hù)的pool集合釋放,交給操作系統(tǒng)了,從而將"可用"狀態(tài)轉(zhuǎn)化為"未使用"狀態(tài)。而當(dāng)python處理完pool,就開始處理arena了。

而對(duì)arena的處理實(shí)際上分為了4中情況

1.如果arena中所有的pool都是empty的,釋放pool集合所占用的內(nèi)存

2.如果之前arena中沒有了empty的pool,那么在"可用"鏈表中就找不到該arena,由于現(xiàn)在arena中有了一個(gè)pool,所以需要將這個(gè)arena鏈入到"可用"鏈表的表頭

3.如果arena中的empty的pool的個(gè)數(shù)為n,那么會(huì)從"可用"arena鏈表中開始尋找arena可以插入的位置,將arena插入到"可用"鏈表。這樣操作的原因就在于"可用"arena鏈表實(shí)際上是一個(gè)有序的鏈表,從表頭開始往后,每一個(gè)arena中empty的pool的個(gè)數(shù),即nfreepools,都不能大于前面的arena,也不能小于后面的arena。保持這樣有序性的原則是分配block時(shí),是從"可用"鏈表的表頭開始尋找可用arena的,這樣就能保證如果一個(gè)arena的empty pool數(shù)量越多,它被使用的機(jī)會(huì)就越少。因此它最終釋放其維護(hù)的pool集合的內(nèi)存的機(jī)會(huì)就越大,這樣就能保證多余的內(nèi)存會(huì)被歸還給操作系統(tǒng)

4.其他情況,則不對(duì)arena進(jìn)行任何處理。

內(nèi)存池全景

前面我們已經(jīng)提到了,對(duì)于一個(gè)用c開發(fā)的龐大的軟件(python是一門高級(jí)語言,但是執(zhí)行對(duì)應(yīng)代碼的解釋器則可以看成是c的一個(gè)軟件),其中的內(nèi)存管理可謂是最復(fù)雜、最繁瑣的地方了。不同尺度的內(nèi)存會(huì)有不同的抽象,這些抽象在各種情況下會(huì)組成各式各樣的鏈表,非常復(fù)雜。但是我們還是有可能從一個(gè)整體的尺度上把握整個(gè)內(nèi)存池,盡管不同的鏈表變幻無常,但我們只需記住,所有的內(nèi)存都在arenas(或者說那個(gè)存放多個(gè)arena的數(shù)組)的掌握之中 。

17.3 循環(huán)引用之垃圾回收

17.3.1 引用計(jì)數(shù)之垃圾回收

現(xiàn)在絕大部分語言都實(shí)現(xiàn)了垃圾回收機(jī)制,也包括python。然而python的垃圾回收和java,c#等語言有一個(gè)很大的不同,那就是python中大多數(shù)對(duì)象的生命周期是通過對(duì)象的引用計(jì)數(shù)來管理的,這一點(diǎn)在開始的章節(jié)我們就說了,對(duì)于python中最基礎(chǔ)的對(duì)象PyObject,有兩個(gè)屬性,一個(gè)是該對(duì)象的類型,還有一個(gè)就是引用計(jì)數(shù)(ob_refcnt)。不過從廣義上將,引用計(jì)數(shù)也算是一種垃圾回收機(jī)制,而且它是一中最簡(jiǎn)單最直觀的垃圾回收計(jì)數(shù)。盡管需要一個(gè)值來維護(hù)引用計(jì)數(shù),但是引用計(jì)數(shù)有一個(gè)最大的優(yōu)點(diǎn):實(shí)時(shí)性。任何內(nèi)存,一旦沒有指向它的引用,那么就會(huì)被回收。而其他的垃圾回收技術(shù)必須在某種特定條件下(比如內(nèi)存分配失敗)才能進(jìn)行無效內(nèi)存的回收。

引用計(jì)數(shù)機(jī)制所帶來的維護(hù)引用計(jì)數(shù)的額外操作,與python運(yùn)行中所進(jìn)行的內(nèi)存分配、釋放、引用賦值的次數(shù)是成正比的。這一點(diǎn),相對(duì)于主流的垃圾回收技術(shù),比如標(biāo)記--清除(mark--sweep)、停止--復(fù)制(stop--copy)等方法相比是一個(gè)弱點(diǎn),因?yàn)樗鼈儙眍~外操作只和內(nèi)存數(shù)量有關(guān),至于多少人引用了這塊內(nèi)存則不關(guān)心。因此為了與引用計(jì)數(shù)搭配、在內(nèi)存的分配和釋放上獲得最高的效率,python設(shè)計(jì)了大量的內(nèi)存池機(jī)制,比如小整數(shù)對(duì)象池、字符串的intern機(jī)制,列表的freelist緩沖池等等,這些大量使用的面向特定對(duì)象的內(nèi)存池機(jī)制正是為了彌補(bǔ)引用計(jì)數(shù)的軟肋。

其實(shí)對(duì)于現(xiàn)在的cpu和內(nèi)存來說,上面的問題都不是什么問題。但是引用計(jì)數(shù)還存在一個(gè)致命的缺陷,這一缺陷幾乎將引用計(jì)數(shù)機(jī)制在垃圾回收技術(shù)中判處了"死刑",這一技術(shù)就是"循環(huán)引用"。而且也正是因?yàn)?#34;循環(huán)引用"這個(gè)致命傷,導(dǎo)致在狹義上并不把引用計(jì)數(shù)機(jī)制看成是垃圾回收技術(shù)

在介紹循環(huán)引用之前,先來看看python引用計(jì)數(shù)什么時(shí)候會(huì)增加,什么時(shí)候會(huì)減少。

引用計(jì)數(shù)加一

對(duì)象被創(chuàng)建:a=1

對(duì)象被引用:b=a

對(duì)象被作為參數(shù)傳到一個(gè)函數(shù)中,func(a)

對(duì)象作為列表、元組等其他容器里面的元素

引用計(jì)數(shù)減一

對(duì)象別名被顯式的銷毀:del a

對(duì)象的引用指向了其他的對(duì)象:a=2

對(duì)象離開了它的作用域,比如函數(shù)的局部變量,在函數(shù)執(zhí)行完畢的時(shí)候,也會(huì)被銷毀(如果沒有獲取棧幀的話),而全局變量則不會(huì)

對(duì)象所在的容器被銷毀,或者從容器中刪除等等

查看引用計(jì)數(shù)

查看一個(gè)對(duì)象的引用計(jì)數(shù),可以通過sys.getrefcount(obj),但是由于作為getrefcount這個(gè)函數(shù)的參數(shù),所以引用計(jì)數(shù)會(huì)多1。

我們之前說,a =?"mashiro",相當(dāng)于把a(bǔ)和a對(duì)應(yīng)的值組合起來放在了命名空間里面,那么你認(rèn)為這個(gè)a對(duì)應(yīng)的值是什么呢?難道是"mashiro"這個(gè)字符串嗎?其實(shí)從python的層面上來看的話確實(shí)是這樣,但是在python的底層,其實(shí)存儲(chǔ)的是字符數(shù)組"mashiro"對(duì)應(yīng)地址,我總覺得前面章節(jié)好像說錯(cuò)了。

b=a在底層中則表示把a(bǔ)的指針拷貝給了b,是的你沒有看錯(cuò),都說python傳遞的是符號(hào),但是在底層就是傳遞了一個(gè)指針,無論什么傳遞的都是指針,在python的層面上傳遞就是符號(hào)、或者就是引用。所以我們看到, 每當(dāng)多了一個(gè)引用,那么"mashiro"(在c的層面上是一個(gè)結(jié)構(gòu)體,PyUnicodeObject)的引用計(jì)數(shù)就會(huì)加1.

而每當(dāng)減少一個(gè)引用,引用計(jì)數(shù)就會(huì)減少1。盡管我們用sys.getrefcount得到的結(jié)果是2,但是當(dāng)這個(gè)函數(shù)執(zhí)行完,由于局部變量的銷毀,其實(shí)結(jié)果已經(jīng)變成了1。因此引用計(jì)數(shù)很方便,就是當(dāng)一片空間沒有人引用了,那么就直接銷毀。盡管維護(hù)這個(gè)引用計(jì)數(shù)需要消耗資源,可還是那句話,對(duì)于如今的硬件資源來說,是完全可以接受的,畢竟引用計(jì)數(shù)真的很方便。但是,是的我要說但是了,就是我們之前的那個(gè)循環(huán)引用的問題。

l1 = []

l2 = []

l1.append(l2)

l2.append(l1)

del l1, l2

初始的時(shí)候,l1和l2指向的內(nèi)存的引用計(jì)數(shù)都為1,但是l1.append(l2),那么l2指向內(nèi)存的引用計(jì)數(shù)變成了2,同理l2.append(l1)導(dǎo)致l1指向內(nèi)存的引用計(jì)數(shù)也變成了2。因此當(dāng)我們del l1,?l2的時(shí)候,引用計(jì)數(shù)會(huì)從2變成1,因此l1和l2都不會(huì)被回收,因?yàn)槲覀兪窍M厥誰1和l2的,但是如果只有引用計(jì)數(shù)的話,那么顯然這兩者是回收不了的。因此這算是引用計(jì)數(shù)的最大的缺陷,因?yàn)闀?huì)導(dǎo)致內(nèi)存泄漏。因此python為了解決這個(gè)問題,就必須在引用計(jì)數(shù)機(jī)制之上又引入了新的主流垃圾回收計(jì)數(shù):標(biāo)記--清除和分代收集計(jì)數(shù)來彌補(bǔ)這個(gè)最致命的漏洞。

17.3.2 三色標(biāo)記模型

無論何種垃圾回收機(jī)制,一般都分為兩個(gè)階段:垃圾檢測(cè)和垃圾回收。垃圾檢測(cè)是從所有的已經(jīng)分配的內(nèi)存中區(qū)別出"可回收"和"不可回收"的內(nèi)存,而垃圾回收則是使操作系統(tǒng)重新掌握垃圾檢測(cè)階段所標(biāo)識(shí)出來的"可回收"內(nèi)存塊。所以垃圾回收,并不是說直接把這塊內(nèi)存的數(shù)據(jù)清空了,而是說將使用權(quán)從新交給了操作系統(tǒng),不會(huì)自己霸占了。下面我們來看看標(biāo)記--清除(mark--sweep)方法是如何實(shí)現(xiàn)的,并為這個(gè)過程建立一個(gè)三色標(biāo)記模型,python中的垃圾回收正是基于這個(gè)模型完成的。

從具體的實(shí)現(xiàn)上來講,標(biāo)記--清除方法同樣遵循垃圾回收的兩個(gè)階段,其簡(jiǎn)要過程如下:

尋找根對(duì)象(root object)的集合,所謂的root object就是一些全局引用和函數(shù)棧的引用。這些引用所用的對(duì)象是不可被刪除的,而這個(gè)root object集合也是垃圾檢測(cè)動(dòng)作的起點(diǎn)

從root object集合出發(fā),沿著root object集合中的每一個(gè)引用,如果能到達(dá)某個(gè)對(duì)象A,則稱A是可達(dá)的(reachable),可達(dá)的對(duì)象也不可被刪除。這個(gè)階段就是垃圾檢測(cè)階段

當(dāng)垃圾檢測(cè)階段結(jié)束后,所有的對(duì)象分為了可達(dá)的(reachable)和不可達(dá)的(unreachable)。而所有可達(dá)對(duì)象都必須予以保留,而不可達(dá)對(duì)象所占用的內(nèi)存將被回收。

在垃圾回收動(dòng)作被激活之前,系統(tǒng)中所分配的所有對(duì)象和對(duì)象之間的引用組成了一張有向圖,其中對(duì)象是圖中的節(jié)點(diǎn),而對(duì)象間的引用則是圖的邊。我們?cè)谶@個(gè)有向圖的基礎(chǔ)之上建立一個(gè)三個(gè)標(biāo)注模型,更形象的展示垃圾回收的整個(gè)動(dòng)作。當(dāng)垃圾回收開始時(shí),我們假設(shè)系統(tǒng)中的所有對(duì)象都是不可達(dá)的,對(duì)應(yīng)在有向圖上就是白色 。隨后從垃圾回收的動(dòng)作開始,沿著始于root object集合中的某個(gè)object的引用鏈,在某個(gè)時(shí)刻到達(dá)了對(duì)象A,那我們把A標(biāo)記為灰色,灰色表示一個(gè)對(duì)象是可達(dá)的,但是其包含的引用還沒有被檢查。當(dāng)我們檢查了對(duì)象A所包含的所有引用之后,A將被標(biāo)記為黑色,表示其包含的所有引用已經(jīng)被檢查過了。顯然,此時(shí)A中引用的對(duì)象則被標(biāo)記成了灰色。假如我們從root object集合開始,按照廣度優(yōu)先的策略進(jìn)行搜索的話,那么不難想象,灰色節(jié)點(diǎn)對(duì)象集合就如同波紋一樣,不斷向外擴(kuò)散,隨著所有的灰色節(jié)點(diǎn)都變成了黑色節(jié)點(diǎn),也就意味著垃圾檢測(cè)階段結(jié)束了。

17.4 python中的垃圾回收

如之前所說,python中主要的內(nèi)存管理手段是引用計(jì)數(shù)機(jī)制,而標(biāo)記--清除和分代收集只是為了打破循環(huán)引用而引入的補(bǔ)充技術(shù)。這一事實(shí)意味著python中的垃圾回收只關(guān)注可能會(huì)產(chǎn)生循環(huán)引用的對(duì)象,而像PyLongObject、PyUnicodeObject這些對(duì)象是絕對(duì)不可能產(chǎn)生循環(huán)引用的,因?yàn)樗鼈儍?nèi)部不可能持有對(duì)其他對(duì)象的引用,所以這些直接通過引用計(jì)數(shù)機(jī)制就可以實(shí)現(xiàn),而且后面我們說的垃圾回收也專指那些可能產(chǎn)生循環(huán)引用的對(duì)象。python中的循環(huán)引用只會(huì)總是發(fā)生在container對(duì)象之間,所謂container對(duì)象就是內(nèi)部可持有對(duì)其他對(duì)象的引用的對(duì)象,比如list、dict、class、instance等等。當(dāng)python開始垃圾回收機(jī)制開始運(yùn)行時(shí),只需要檢查這些container對(duì)象,而對(duì)于PyLongObject、PyUnicodeObject則不需要理會(huì),這使得垃圾回收帶來的開銷只依賴于container對(duì)象的數(shù)量,而非所有對(duì)象的數(shù)量。為了達(dá)到這一點(diǎn),python就必須跟蹤所創(chuàng)建的每一個(gè)container對(duì)象,并將這些對(duì)象組織到一個(gè)集合中,只有這樣,才能將垃圾回收的動(dòng)作限制在這些對(duì)象上。而python采用了一個(gè)雙向鏈表,所有的container對(duì)象在創(chuàng)建之后,都會(huì)被插入到這個(gè)鏈表當(dāng)中。

17.4.1 可收集對(duì)象鏈表

在對(duì)python對(duì)象機(jī)制的分析當(dāng)中我們已經(jīng)看到,任何一個(gè)python對(duì)象都可以分為兩部分,一部分是PyObject_HEAD,另一部分是對(duì)象自身的數(shù)據(jù)。然而對(duì)于一個(gè)需要被垃圾回收機(jī)制跟蹤的container來說,還不夠,因?yàn)檫@個(gè)對(duì)象還必須鏈入到python內(nèi)部的可收集對(duì)象鏈表中。而一個(gè)container對(duì)象要想成為一個(gè)可收集的對(duì)象,則必須加入額外的信息,這個(gè)信息位于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;

所以,對(duì)于python所創(chuàng)建的可收集container對(duì)象,其內(nèi)存分布與我們之前所了解的內(nèi)存布局是不同的,我們可以從可收集container對(duì)象的創(chuàng)建過程中窺見其內(nèi)存分布。

//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;

//將對(duì)象和PyGC_Head所需內(nèi)存加起來

if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))

return PyErr_NoMemory();

size = sizeof(PyGC_Head) + basicsize;

//為對(duì)象本身和PyGC_Head申請(qǐng)內(nèi)存

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;

}

因此我們可以很清晰的看到,當(dāng)python為可收集的container對(duì)象申請(qǐng)內(nèi)存空間時(shí),為PyGC_Head也申請(qǐng)了空間,并且其位置位于container對(duì)象之前。所以對(duì)于PyListObject、PyDictObject等container對(duì)象的內(nèi)存分布的推測(cè)就應(yīng)該變成這樣。

在可收集container對(duì)象的內(nèi)存分布中,內(nèi)存分為三個(gè)部分,首先第一塊用于垃圾回收機(jī)制,然后緊跟著的是python中所有對(duì)象都會(huì)有的PyObject_HEAD,最后才是container自身的數(shù)據(jù)。這里的container對(duì)象,既可以是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的模樣,里面除了兩個(gè)建立鏈表結(jié)構(gòu)的前向和后向指針外,還有一個(gè)gc_ref,而這個(gè)值被初始化為GC_UNTRACKED,在上面的代碼中可以看到。這個(gè)變量對(duì)于垃圾回收的運(yùn)行至關(guān)重要,但是在分析它之前我們還需要了解一些其他的東西。

當(dāng)垃圾回收機(jī)制運(yùn)行期間,我們需要在一個(gè)可收集的container對(duì)象的PyGC_Head部分和PyObject_HEAD部分之間來回切換。更清楚的說,某些時(shí)候,我們持有一個(gè)對(duì)象A的PyObject_HEAD的地址,但是我們需要根據(jù)這個(gè)地址來獲得PyGC_Head的地址;而且某些時(shí)候,我們又需要反過來進(jìn)行逆運(yùn)算。而python提供了兩個(gè)地址之間的轉(zhuǎn)換算法

//gcmodule.c

//AS_GC,根據(jù)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中,出現(xiàn)了用于建立鏈表的兩個(gè)指針,只有將創(chuàng)建的可收集container對(duì)象鏈接到python內(nèi)部維護(hù)的可收集對(duì)象鏈表中,python的垃圾回收機(jī)制才能跟蹤和處理這個(gè)container對(duì)象。但是我們發(fā)現(xiàn),在創(chuàng)建可收集container對(duì)象之時(shí),并沒有立刻將這個(gè)對(duì)象鏈入到鏈表中。實(shí)際上,這個(gè)動(dòng)作是發(fā)生在創(chuàng)建某個(gè)container對(duì)象最后一步,以PyListObject的創(chuàng)建舉例。

//listobject.c

PyObject *

PyList_New(Py_ssize_t size)

{

PyListObject *op;

...

Py_SIZE(op) = size;

op->allocated = size;

//創(chuàng)建PyListObject對(duì)象、并設(shè)置完屬性之后,返回之前,通過這一步_PyObject_GC_TRACK將所創(chuàng)建的container對(duì)象鏈接到了python中的可收集對(duì)象鏈表中。

_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會(huì)將自己的垃圾回收機(jī)制限制在其維護(hù)的可收集對(duì)象鏈表上,因?yàn)樗械难h(huán)引用一定是發(fā)生這個(gè)鏈表的一群對(duì)象之間。在_PyObject_GC_TRACK之后,我們創(chuàng)建的container對(duì)象也就置身于python垃圾回收機(jī)制的掌控機(jī)制當(dāng)中了。

同樣的,python還提供將一個(gè)container對(duì)象從鏈表中摘除的方法,顯然這個(gè)方法應(yīng)該會(huì)在對(duì)象被銷毀的時(shí)候調(diào)用。

//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的逆運(yùn)算而已

17.4.2 分代的垃圾收集

無論什么語言,寫出來的程序都有共同之處。那就是不同對(duì)象的聲明周期會(huì)存在不同,有的對(duì)象所占的內(nèi)存塊的生命周期很短,而有的內(nèi)存塊的生命周期則很長(zhǎng),甚至可能從程序的開始持續(xù)到程序結(jié)束。這兩者的比例大概在80~90%

這對(duì)于垃圾回收機(jī)制有著重要的意義,因?yàn)槲覀円呀?jīng)知道,像標(biāo)記--清除這樣的垃圾回收機(jī)制所帶來的額外操作實(shí)際上是和系統(tǒng)中內(nèi)存塊的數(shù)量是相關(guān)的,當(dāng)需要回收的內(nèi)存塊越多的時(shí)候,垃圾檢測(cè)帶來的額外操作就越多,相反則越少。因此我們可以采用一種空間換時(shí)間的策略,因?yàn)槟壳八袑?duì)象都在一個(gè)鏈子上,每當(dāng)進(jìn)行垃圾回收機(jī)制的時(shí)候,都要把所有對(duì)象都檢查一遍。而其實(shí)也有不少比較穩(wěn)定的對(duì)象(在多次垃圾回收的洗禮下能活下來),我們完全沒有必要每次都檢查,或者說檢查的頻率可以降低一些。于是聰明如你已經(jīng)猜到了,我們?cè)賮硪桓溩硬痪涂梢粤?#xff0c;把那些認(rèn)為比較穩(wěn)定的對(duì)象移到另外一條鏈子上,而新的鏈子進(jìn)行垃圾回收的頻率會(huì)低一些,總之頻率不會(huì)像初始的鏈子那么高。

所以這種思想就是:將系統(tǒng)中的所有內(nèi)存塊根據(jù)其存活時(shí)間劃分為不同的集合,每一個(gè)集合就成為一個(gè)"代",垃圾回收的頻率隨著"代"的存活時(shí)間的增大而減小,也就是說,存活的越長(zhǎng)的對(duì)象就越可能不是垃圾,就越可能是程序中需要一直存在的對(duì)象,就應(yīng)該少去檢測(cè)它。反正不是垃圾,你檢了也白檢。那么關(guān)鍵的問題來了,這個(gè)存活時(shí)間是如何被衡量的呢?或者我們說當(dāng)對(duì)象比較穩(wěn)定的時(shí)候的這個(gè)穩(wěn)定是如何衡量的呢?沒錯(cuò),我們上面已經(jīng)暴露了,就是通過經(jīng)歷了幾次垃圾回收動(dòng)作來評(píng)判,如果一個(gè)對(duì)象經(jīng)歷的垃圾回收次數(shù)越多,那么顯然其存活時(shí)間就越長(zhǎng)。因?yàn)閜ython的垃圾回收器,每當(dāng)條件滿足時(shí)(至于什么條件我們后面會(huì)說),就會(huì)進(jìn)行一次垃圾回收(注意:不同的代的垃圾回收的頻率是不同的),而每次掃黃的時(shí)候你都不在,吭,每次垃圾回收的時(shí)候你都能活下來,這就說明你存活的時(shí)間更長(zhǎng),或者像我們上面說的更穩(wěn)定,那么就不應(yīng)該再把你放在這個(gè)鏈子上了,而是會(huì)移動(dòng)到新的鏈子上。而在新的鏈子上,進(jìn)行垃圾回收的頻率會(huì)降低,因?yàn)榧热环€(wěn)定了,檢測(cè)就不必那么頻繁了,或者說新的鏈子上觸發(fā)垃圾回收所需要的時(shí)間更長(zhǎng)了。

"代"似乎是一個(gè)比較抽象的概念,但在python中,你就把"代"想象成多個(gè)對(duì)象組成集合,或者你把"代"想象成鏈表(或者鏈子)也可以,因?yàn)檫@些對(duì)象都串在鏈表上面。而屬于同一"代"的內(nèi)存塊都被鏈接在同一個(gè)鏈表中。而在python中總共存在三條鏈表,說明python中所有的對(duì)象總共可以分為三代,分別零代、一代、二代。一個(gè)"代"就是一條我們上面提到的可收集對(duì)象鏈表。而在前面所介紹的鏈表的基礎(chǔ)之上,為了支持分代機(jī)制,我們需要的僅僅是一個(gè)額外的表頭而已。

//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);

上面這個(gè)維護(hù)了三個(gè)gc_generation結(jié)構(gòu)的數(shù)組,通過這個(gè)數(shù)組控制了三條可收集對(duì)象鏈表,這就是python中用于分代垃圾收集的三個(gè)"代"。

而我們?cè)谥吧厦嬲f的_PyObject_GC_TRACK中會(huì)看到_PyGC_generation0,它不偏不斜,指向的正是第0代鏈表。

對(duì)于每一個(gè)gc_generation,其中的count記錄了當(dāng)前這條可收集對(duì)象鏈表中一共有多少個(gè)對(duì)象。而在_PyObject_GC_Alloc中我們可以看到每當(dāng)分配了內(nèi)存,就會(huì)進(jìn)行_PyRuntime.gc.generations[0].count++動(dòng)作,將第0代鏈表中所維護(hù)的內(nèi)存塊數(shù)量加1,這預(yù)示著所有新創(chuàng)建的對(duì)象實(shí)際上都會(huì)被加入到0代鏈表當(dāng)中,而這一點(diǎn)也確實(shí)如此,已經(jīng)被_PyObject_GC_TRACK證明了。而且我們發(fā)現(xiàn)這里是先將數(shù)量加1,然后再將新的container對(duì)象(內(nèi)存塊)才會(huì)被鏈接到第0代鏈表當(dāng)中,當(dāng)然這個(gè)無所謂啦。

而gc_generation中的threshold則記錄該條可收集對(duì)象鏈表中最多可以容納多少個(gè)可收集對(duì)象,從python的實(shí)現(xiàn)代碼中,我們知道第0代鏈表中最多可以容納700個(gè)對(duì)象(只可能是container對(duì)象)。而一旦第0代鏈表中的container對(duì)象超過了700個(gè)這個(gè)閾值,那么會(huì)立刻除法垃圾回收機(jī)制。

static Py_ssize_t

collect_generations(void)

{

int i;

Py_ssize_t n = 0;

for (i = NUM_GENERATIONS-1; i >= 0; i--) {

//當(dāng)count大于threshold的時(shí)候,但是這個(gè)僅僅針對(duì)于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;

}

這里面雖然寫了一個(gè)for循環(huán),但是只有當(dāng)?shù)?代鏈表的count超過了threshold的時(shí)候才會(huì)觸發(fā)垃圾回收,那么1代鏈表和2代鏈表觸發(fā)垃圾回收的條件又是什么呢?當(dāng)0代鏈表觸發(fā)了10次垃圾回收的時(shí)候,會(huì)觸發(fā)一次1代鏈表的垃圾回收。當(dāng)1代鏈表觸發(fā)了10次垃圾回收的時(shí)候,會(huì)觸發(fā)一次2代鏈表的垃圾回收。另外:

在清理1代鏈表的時(shí)候,會(huì)順帶清理0代鏈表

在清理2代鏈表的時(shí)候,會(huì)順帶清理0代鏈表和1代鏈表

17.4.3 python中的標(biāo)記--清除

我們上面說到,當(dāng)清理1代鏈表會(huì)順帶清理0代鏈表,總是就是把比自己"代"要小的鏈子也清理了。那么這是怎么做到的呢?其實(shí)答案就在gc_list_merge函數(shù)中,如果清理的是1代鏈表,那么在開始垃圾回收之前,python會(huì)將0代鏈表(比它年輕的),整個(gè)地鏈接到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代鏈表,所以此后的標(biāo)記--清除算法就將在merge之后的那一條鏈表上進(jìn)行。

在介紹python中的標(biāo)記--清除垃圾回收方法之前,我們需要建立一個(gè)循環(huán)引用的最簡(jiǎn)單例子

list1 = []

list2 = []

list1.append(list2)

list2.append(list1)

# 注意這里多了一個(gè)外部引用

a = list1

list3 = []

list4 = []

list3.append(list4)

list4.append(list3)

上面的數(shù)字指的是當(dāng)前對(duì)象的引用計(jì)數(shù)ob_refcnt的值

17.4.3.1 尋找root object集合

為了使用標(biāo)記--清除算法,按照我們之前對(duì)垃圾收集算法的一般性描述,首先我們需要找到root object,那么在我們上面的那幅圖中,哪些是屬于root object呢?

讓我們換個(gè)角度來思考,前面提到,root object是不能被刪除的對(duì)象。也就是說,在可收集對(duì)象鏈表的外部存在著某個(gè)引用在引用這個(gè)對(duì)象,刪除這個(gè)對(duì)象會(huì)導(dǎo)致錯(cuò)誤的行為,那么在我們當(dāng)前這個(gè)例子中只有l(wèi)ist1是屬于root object的。但這僅僅是觀察的結(jié)果,那么如何設(shè)計(jì)一種算法來得到這個(gè)結(jié)果呢?

我們注意到這樣一個(gè)事實(shí),如果兩個(gè)對(duì)象的引用計(jì)數(shù)都為1,但是僅僅它們之間存在著循環(huán)引用,那么這兩個(gè)對(duì)象是需要被回收的,也就是說,盡管它們的引用計(jì)數(shù)表現(xiàn)為非0,但是實(shí)際上有效的引用計(jì)數(shù)為0。這里,我們提出了有效引用計(jì)數(shù)的概念,為了從引用計(jì)數(shù)中獲得優(yōu)秀的引用計(jì)數(shù),必須將循環(huán)引用的影響取出,也就是說,這個(gè)閉環(huán)從引用中摘除,而具體的實(shí)現(xiàn)就是兩個(gè)對(duì)象各自的引用值都減去1。這樣一來,兩個(gè)對(duì)象的引用計(jì)數(shù)都成為了0,這樣我們便揮去了循環(huán)引用的迷霧,是有效引用計(jì)數(shù)出現(xiàn)了真身。那么如何使兩個(gè)對(duì)象的引用計(jì)數(shù)都減1呢,很簡(jiǎn)單,假設(shè)這兩個(gè)對(duì)象為A和B,那么從A出發(fā),由于它有一個(gè)對(duì)B的引用,則將B的引用計(jì)數(shù)減1;然后順著引用達(dá)到B,發(fā)現(xiàn)它有一個(gè)對(duì)A的引用,那么同樣會(huì)將A的引用減1,這樣就完成了循環(huán)引用對(duì)象間環(huán)的刪除。

總結(jié)一下就是,python會(huì)尋找那些具有循環(huán)引用的、但是沒有被外部引用的對(duì)象,并嘗試把它們的引用計(jì)數(shù)都減去1

但是這樣就引出了一個(gè)問題,假設(shè)可收集對(duì)象鏈表中的container對(duì)象A有一個(gè)對(duì)對(duì)象C的引用,而C并不在這個(gè)鏈表中,如果將C的引用計(jì)數(shù)減去1,而最后A并沒有被回收,那么顯然,C的引用計(jì)數(shù)被錯(cuò)誤地減少1,這將導(dǎo)致未來的某個(gè)時(shí)刻對(duì)C的引用會(huì)出現(xiàn)懸空。這就要求我們必須在A沒有被刪除的情況下回復(fù)C的引用計(jì)數(shù),可是如果采用這樣的方案的話,那么維護(hù)引用計(jì)數(shù)的復(fù)雜度將成倍增長(zhǎng)。換一個(gè)角度,其實(shí)我們有更好的做法,我們不改動(dòng)真實(shí)的引用計(jì)數(shù),而是改動(dòng)引用計(jì)數(shù)的副本。對(duì)于副本,我們無論做什么樣的改動(dòng),都不會(huì)影響對(duì)象生命周期的維護(hù),因?yàn)檫@個(gè)副本的唯一作用就是尋找root? object集合,而這個(gè)副本就是PyGC_Head中的gc.gc_ref。在垃圾回收的第一步,就是遍歷可收集對(duì)象鏈表,將每個(gè)對(duì)象的gc.gc_ref的值設(shè)置為其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);

}

}

//而接下來的動(dòng)作就是要將環(huán)引用從引用中摘除

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);

}

}

我們注意到里面有一個(gè)traverse,這個(gè)是和特定的container 對(duì)象有關(guān)的,在container對(duì)象的類型對(duì)象中定義。一般來說,traverse的動(dòng)作就是遍歷container對(duì)象中的每一個(gè)引用,然后對(duì)引用進(jìn)行某種動(dòng)作,而這個(gè)動(dòng)作在subtract_refs中就是visit_decref,它以一個(gè)回調(diào)函數(shù)的形式傳遞到traverse操作中。比如:我們來看看PyListObject對(duì)象所定義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; )

//對(duì)列表中的每一個(gè)元素都進(jìn)行回調(diào)的操作

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指向的對(duì)象是不是被垃圾收集監(jiān)控的

//標(biāo)識(shí)container對(duì)象是被垃圾收集監(jiān)控的

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之后,可收集對(duì)象鏈表中所有container對(duì)象之間的環(huán)引用就被摘除了。這時(shí)有一些container對(duì)象的PyGC_Head.gc_ref還不為0,這就意味著存在對(duì)這些對(duì)象的外部引用,這些對(duì)象就是開始標(biāo)記--清除算法的root object。

估計(jì)有人不明白引用計(jì)數(shù)是加在什么地方,其實(shí)變量=值在python中,變量得到的都是值的指針,a =?1,表示是在命名空間里面會(huì)有"a": 1這個(gè)鍵值對(duì),但看似是這樣,其實(shí)存儲(chǔ)的并不是1,而是1這個(gè)結(jié)構(gòu)體(python對(duì)象在底層是一個(gè)結(jié)構(gòu)體)的指針,這個(gè)結(jié)構(gòu)體存儲(chǔ)在堆區(qū)。我們獲取a的引用計(jì)數(shù),其實(shí)是獲取a指向的這個(gè)對(duì)象的引用計(jì)數(shù),此時(shí)為1,如果b=a,在底層就等價(jià)于把a(bǔ)存儲(chǔ)的內(nèi)容(指針)拷貝給了b,那么此時(shí)a和b存儲(chǔ)的指針指的都是同一個(gè)對(duì)象,那么這個(gè)對(duì)象的引用計(jì)數(shù)就變成了2。如果再來個(gè)b=2,那么表示再創(chuàng)建一個(gè)結(jié)構(gòu)體存儲(chǔ)的值為2,然后讓b存儲(chǔ)新的結(jié)構(gòu)體的指針。那么原來的結(jié)構(gòu)體的引用計(jì)數(shù)就從2又變成了1。

所以為什么初始的時(shí)候,list1的引用計(jì)數(shù)是3就很明顯了,list1的引用計(jì)數(shù)指的其實(shí)是list1這個(gè)變量對(duì)應(yīng)的值(或者說在底層,list1存儲(chǔ)的指針指向的值)的引用計(jì)數(shù),所以一旦創(chuàng)建一個(gè)變量那么引用計(jì)數(shù)會(huì)自動(dòng)增加為1,然后a也指向了list1所指向的內(nèi)存,并且list1又作為list2的一個(gè)元素(這個(gè)位置的元素存儲(chǔ)了指向list1的指針),所以引用計(jì)數(shù)總共是3。

由于sys.getrefcount函數(shù)本身會(huì)多一個(gè)引用,所以減去1的話,那么都是3。表示它們指向的內(nèi)存存儲(chǔ)的值的引用計(jì)數(shù)為3。sys.getrefcount(a) -> 4,這個(gè)時(shí)候a就想到了,除了我,還有兩位老鐵指向了我指向的內(nèi)存。

17.4.3.2 垃圾標(biāo)記

假設(shè)我們現(xiàn)在執(zhí)行了刪除操作del?list1, list2,?list3, list4,那么成功地尋找到root object集合之后,我們就可以從root object觸發(fā),沿著引用鏈,一個(gè)接一個(gè)地標(biāo)記不能回收的內(nèi)存,由于root object集合中的對(duì)象是不能回收的,因此,被這些對(duì)象直接或間接引用的對(duì)象也是不能回收的,比如這里的list2,即便del list2,但是因?yàn)閘ist1不能回收,而又append了list2,所以list2指向的內(nèi)存也是不可以釋放的。下面在從root object出發(fā)前,我們首先需要將現(xiàn)在的內(nèi)存鏈表一分為二,一條鏈表維護(hù)root object集合,成為root鏈表,而另一條鏈表中維護(hù)剩下的對(duì)象,成為unreachable鏈表。之所以要分解成兩個(gè)鏈表,是出于這樣一種考慮:顯然,現(xiàn)在的unreachable鏈表是名不副實(shí)的,因?yàn)槔锩婵赡艽嬖诒籸oot鏈表中的對(duì)象直接或者間接引用的對(duì)象,這些對(duì)象也是不可以回收的,因此一旦在標(biāo)記中發(fā)現(xiàn)了這樣的對(duì)象,那么就應(yīng)該將其從unreachable中移到root鏈表中;當(dāng)完成標(biāo)記之后,unreachable鏈表中剩下的對(duì)象就是名副其實(shí)的垃圾對(duì)象了,那么接下來的垃圾回收只需要限制在unreachable鏈表中即可。

為此python專門準(zhǔn)備了一條名為unreachable的鏈表,通過move_unreachable函數(shù)完成了對(duì)原始鏈表的切分。

//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);

//設(shè)置其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]:對(duì)于非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]:對(duì)于還沒有處理的對(duì)象,恢復(fù)其gc_refs

if (gc_refs == 0) {

_PyGCHead_SET_REFS(gc, 1);

}

//[4]:對(duì)于已經(jīng)被挪到unreachable鏈表中的對(duì)象,將其再次挪動(dòng)到原來的鏈表

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中,沿著可收集對(duì)象鏈表依次向前,并檢查其PyGC_Head.gc.gc_ref值,我們發(fā)現(xiàn)這里的動(dòng)作是遍歷鏈表,而并非從root object集合出發(fā),遍歷引用鏈。這會(huì)導(dǎo)致一個(gè)微妙的結(jié)果,即當(dāng)檢查到一個(gè)gc_ref為0的對(duì)象時(shí),我們并不能立即斷定這個(gè)對(duì)象就是垃圾對(duì)象。因?yàn)樵谶@個(gè)對(duì)象之后的對(duì)象鏈表上,也許還會(huì)遇到一個(gè)root object,而這個(gè)root object引用該對(duì)象。所以這個(gè)對(duì)象只是一個(gè)可能的垃圾對(duì)象,因此我們才要將其標(biāo)志為GC_TENTATIVELY_UNREACHABLE,但是還是通過gc_list_move將其搬到了unreachable鏈表中,咦,難道不會(huì)出問題嗎?別急,我們馬上就會(huì)看到, python還留了后手。

當(dāng)在move_unreachable中遇到一個(gè)gc_refs不為0的對(duì)象A時(shí),顯然,A是root object或者是從某個(gè)root object開始可以引用到的對(duì)象,而A所引用的所有對(duì)象也都是不可回收的對(duì)象。因此在代碼的[1]處下面,我們看到會(huì)再次調(diào)用與特定對(duì)象相關(guān)的transverse操作,依次對(duì)A所引用的對(duì)象調(diào)用visit_reachable。在visit_reachable的[4]處我們發(fā)現(xiàn),如果A所引用的對(duì)象之前曾被標(biāo)注為GC_TENTATIVELY_UNREACHABLE,那么現(xiàn)在A可以訪問到它,意味著它也是一個(gè)不可回收的對(duì)象,所以python會(huì)再次從unreachable鏈表中將其搬回到原來的鏈表。注意:這里的reachable,就是move_unreachable中的young,也就是我們所謂的root object鏈表。python還會(huì)將其gc_refs設(shè)置為1,表示該對(duì)象是一個(gè)不可回收對(duì)象。同樣在[1]處,我們看到對(duì)A所引用的gc_refs為0的對(duì)象,其gc_refs也被設(shè)置成了1。想一想這是什么對(duì)象呢?顯然它就是在鏈表move_unreachable操作中還沒有訪問到的對(duì)象,這樣python就直接掐斷了之后move_unreachable訪問它時(shí)將其移動(dòng)到unreachable鏈表的誘因。

當(dāng)move_unreachable完成之后,最初的一條鏈表就被切分成了兩條鏈表,在unreachable鏈表中,就是我們發(fā)現(xiàn)的垃圾對(duì)象,是垃圾回收的目標(biāo)。但是等一等,在unreachable鏈表中,所有的對(duì)象都可以安全回收嗎?其實(shí),垃圾回收在清理對(duì)象的時(shí)候,默認(rèn)是會(huì)清理的,但是一旦當(dāng)我們定義了函數(shù)__del__,那么在清理對(duì)象的時(shí)候就會(huì)調(diào)用這個(gè)__del__方法,因此也叫析構(gòu)函數(shù),這是python為開發(fā)人員提供的在對(duì)象被銷毀時(shí)進(jìn)行某些資源釋放的Hook機(jī)制。在python3中,即使我們重寫了也沒事,因?yàn)閜ython會(huì)把含有__del__函數(shù)的PyInstanceObject對(duì)象都統(tǒng)統(tǒng)移動(dòng)到一個(gè)名為garbage的PyListObject對(duì)象中。

17.4.4.4 垃圾回收

要回收unreachable鏈表中的垃圾對(duì)象,就必須先打破對(duì)象間的循環(huán)引用,前面我們已經(jīng)闡述了如何打破循環(huán)引用的辦法,下面來看看具體的銷毀過程

//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);

}

}

}

其中會(huì)調(diào)用container對(duì)象的類型對(duì)象中的tp_clear操作,這個(gè)操作會(huì)調(diào)整container對(duì)象中引用的對(duì)象的引用計(jì)數(shù)值,從而打破完成循環(huán)的最終目標(biāo)。還是以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調(diào)整為0

Py_SIZE(a) = 0;

//ob_item是一個(gè)二級(jí)指針,本來指向一個(gè)數(shù)組的指針

//現(xiàn)在指向?yàn)镹ULL

a->ob_item = NULL;

//容量也設(shè)置為0

a->allocated = 0;

while (--i >= 0) {

//數(shù)組里面元素也全部減少引用計(jì)數(shù)

Py_XDECREF(item[i]);

}

//釋放數(shù)組

PyMem_FREE(item);

}

return 0;

}

我們注意到,在delete_garbage中,有一些unreachable鏈表中的對(duì)象會(huì)被重新送回到reachable鏈表(即delete_garbage的old參數(shù))中,這是由于進(jìn)行clear動(dòng)作時(shí),如果成功進(jìn)行,則通常一個(gè)對(duì)象會(huì)把自己從垃圾回收機(jī)制維護(hù)的鏈表中摘除(也就是這里的collectable鏈表)。由于某些原因,對(duì)象可能在clear動(dòng)作時(shí),沒有成功完成必要的動(dòng)作,從而沒有將自己從collectable鏈表摘除,這表示對(duì)象認(rèn)為自己還不能被銷毀,所以python需要講這種對(duì)象放回到reachable鏈表中。

我們?cè)谏厦婵吹搅薼ist_clear,假設(shè)是調(diào)用了list3的list_clear,那么不好意思,這個(gè)是對(duì)list4做的處理。因?yàn)閘ist3和list4存在循環(huán)引用,如果調(diào)用了list3的list_clear會(huì)減少list4的引用計(jì)數(shù),由于這兩位老鐵都被刪除了,還惺惺相惜賴在內(nèi)存里面不走,所以將list4的引用計(jì)數(shù)減少1之后,只能歸于湮滅了,然后會(huì)調(diào)用其list_dealloc,注意:這時(shí)候調(diào)用的是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) {

//依次遍歷,減少內(nèi)部元素的引用計(jì)數(shù)

i = Py_SIZE(op);

while (--i >= 0) {

Py_XDECREF(op->ob_item[i]);

}

//釋放內(nèi)存

PyMem_FREE(op->ob_item);

}

//緩沖池機(jī)制

if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))

free_list[numfree++] = op;

else

Py_TYPE(op)->tp_free((PyObject *)op);

Py_TRASHCAN_SAFE_END(op)

}

我們知道調(diào)用list3的list_clear,減少內(nèi)部元素引用計(jì)數(shù)的時(shí)候,導(dǎo)致list4引用計(jì)數(shù)為0。而一旦list4的引用計(jì)數(shù)為0,那么是不是也要執(zhí)行和list3一樣的list_clear動(dòng)作呢?然后會(huì)發(fā)現(xiàn)list3的引用計(jì)數(shù)也為0了,因此list3也會(huì)被銷毀。循環(huán)引用,彼此共生,銷毀之路,怎能獨(dú)自前行?最終list3和list4都會(huì)執(zhí)行內(nèi)部的list_dealloc,釋放內(nèi)部元素,調(diào)整參數(shù),當(dāng)然還有所謂的緩沖池機(jī)制等等。總之如此一來,list3和list4就都被安全地回收了。

17.4.4.5 總結(jié)

雖然有很多對(duì)象掛在垃圾收集機(jī)制監(jiān)控的鏈表上,但是很多時(shí)候是引用計(jì)數(shù)機(jī)制在維護(hù)這些對(duì)象,只有引用計(jì)數(shù)無能為力的循環(huán)引用,垃圾收集機(jī)制才會(huì)起到作用(這里沒有把引用計(jì)數(shù)機(jī)制看成垃圾回收,當(dāng)然如果別人問你python的垃圾回收機(jī)制的時(shí)候,你也可以把引用計(jì)數(shù)機(jī)制加上)。事實(shí)上,如果不是循環(huán)引用的話,那么垃圾回收是無能為力的,因?yàn)閽煸诶厥諜C(jī)制上的對(duì)象都是引用計(jì)數(shù)不為0的,如果為0早被引用計(jì)數(shù)機(jī)制干掉了。而引用計(jì)數(shù)不為0的情況只有兩種:一種是被程序使用的對(duì)象,二是循環(huán)引用中的對(duì)象。被程序使用的對(duì)象是不能被回收的,所以垃圾回收只能處理那些循環(huán)引用的對(duì)象。

所以python的垃圾回收就是:引用計(jì)數(shù)為主,分代回收為輔,兩者結(jié)合使用,后者主要是為了彌補(bǔ)前者的缺點(diǎn)而存在的。

17.5 python中的gc模塊

這個(gè)gc模塊,底層就是gcmodule,我們說這些模塊底層是用c寫的,當(dāng)python編譯好時(shí),就內(nèi)嵌在解釋器里面了。我們可以導(dǎo)入它,但是在python安裝目錄上看不到。

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

這個(gè)函數(shù)表示開啟垃圾回收機(jī)制,默認(rèn)是自動(dòng)開啟的。

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

import gc

class A:

pass

# 關(guān)掉gc

gc.disable()

while True:

a1 = A()

a2 = A()

# 此時(shí)內(nèi)部出現(xiàn)了循環(huán)引用

a1.__dict__["attr"] = a2

a2.__dict__["attr"] = a1

# 由于循環(huán)引用,此時(shí)是del a1, a2,光靠引用計(jì)數(shù)是刪不掉的

# 需要垃圾回收,但是我們給關(guān)閉了

del a1, a2

無限循環(huán),并且每次循環(huán)都會(huì)創(chuàng)建新的對(duì)象,最終導(dǎo)致內(nèi)存無限增大。

import gc

class A:

pass

# 關(guān)掉gc

gc.disable()

while True:

a1 = A()

a2 = A()

這里即使我們關(guān)閉了gc,但是每一次循環(huán)都會(huì)指向一個(gè)新的對(duì)象,而之前的對(duì)象由于沒有人指向了,那么引用計(jì)數(shù)為0,直接就被引用計(jì)數(shù)機(jī)制干掉了,內(nèi)存會(huì)一直穩(wěn)定,不會(huì)出現(xiàn)增長(zhǎng)。所以我們看到,即使關(guān)閉了gc,但是對(duì)于那些引用計(jì)數(shù)為0的,該刪除還是會(huì)刪除的。所以引用計(jì)數(shù)很簡(jiǎn)單,就是按照對(duì)應(yīng)的規(guī)則該加1加1,該減1減1,一旦為0直接銷毀。而當(dāng)出現(xiàn)循環(huán)引用的時(shí)候,才需要gc閃亮登場(chǎng)。這里關(guān)閉了gc,但是沒有循環(huán)引用所以沒事,而上一個(gè)例子,關(guān)閉了gc,但是出現(xiàn)了循環(huán)引用,而引用計(jì)數(shù)機(jī)制只會(huì)根據(jù)引用計(jì)數(shù)來判斷,而發(fā)現(xiàn)引用計(jì)數(shù)不為0,所以就一直傻傻地不回收,程序又一直創(chuàng)建新的對(duì)象,最終導(dǎo)致內(nèi)存越用越多。而上一個(gè)例子若是開啟了gc,那么分代回收計(jì)數(shù),就會(huì)通過標(biāo)記--清除的方式將產(chǎn)生循環(huán)引用的對(duì)象的引用計(jì)數(shù)減1,而引用計(jì)數(shù)機(jī)制發(fā)現(xiàn)引用計(jì)數(shù)為0了,那么就會(huì)將對(duì)象回收掉。所以這個(gè)引用計(jì)數(shù)機(jī)制到底算不算垃圾回收機(jī)制的一種呢?你要說算吧,我把gc關(guān)閉了,引用計(jì)數(shù)機(jī)制還可以發(fā)揮作用,你要說不算吧,它確實(shí)是負(fù)責(zé)判定對(duì)象是否應(yīng)該被回收的唯一標(biāo)準(zhǔn),所以該怎么說就具體看情況吧。

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

import gc

print(gc.isenabled()) # True

gc.disable()

print(gc.isenabled()) # False

gc.collect():立刻觸發(fā)垃圾回收

我們說,垃圾回收觸發(fā)是需要條件的,比如0代鏈表,清理零代鏈表的時(shí)候,需要對(duì)象的個(gè)數(shù)count大于閾值threshold(默認(rèn)是700),但是這個(gè)函數(shù)可以強(qiáng)制觸發(fā)垃圾回收。

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

import gc

print(gc.get_threshold()) # (700, 10, 10)

# 700:零代鏈表的對(duì)象超過700個(gè),觸發(fā)垃圾回收

# 10:零代鏈表,垃圾回收10次,會(huì)清理一代鏈表

# 10:一代鏈表,垃圾回收10次,會(huì)清理二代鏈表

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

import gc

gc.set_threshold(1000, 100, 100)

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

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

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():返回被垃圾回收器追蹤的所有對(duì)象,一個(gè)列表

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

import gc

a = 1

b = []

print(gc.is_tracked(a)) # False

print(gc.is_tracked(b)) # True

# 我們說只有那些可能會(huì)產(chǎn)生循環(huán)引用的對(duì)象才會(huì)被垃圾回收器跟蹤

gc.get_referrers(obj):返回所有引用了obj的對(duì)象

gc.get_referents(obj):返回所有被obj引用了的對(duì)象

gc.freeze():凍結(jié)所有被垃圾回收器跟蹤的對(duì)象并在以后的垃圾回收中不被處理

gc.unfreeze():取消所有凍結(jié)的對(duì)象,讓它們繼續(xù)參數(shù)垃圾回收

gc.get_freeze_count():獲取凍結(jié)的對(duì)象個(gè)數(shù)

import gc

# 不需要參數(shù),會(huì)自動(dòng)找到被垃圾回收器跟蹤的對(duì)象

gc.freeze()

# 說明有很多內(nèi)置對(duì)象在被跟蹤,被我們凍結(jié)了

print(gc.get_freeze_count()) # 24397

b = []

gc.freeze()

# 只要這里比上面多1個(gè)就行

print(gc.get_freeze_count()) # 24398

# 取消凍結(jié)

gc.unfreeze()

print(gc.get_freeze_count()) # 0

gc.get_debug():獲取debug級(jí)別

import gc

print(gc.get_debug()) # 0

gc.set_debug():設(shè)置debug級(jí)別

import gc

"""

DEBUG_STATS - 在垃圾收集過程中打印所有統(tǒng)計(jì)信息

DEBUG_COLLECTABLE - 打印發(fā)現(xiàn)的可收集對(duì)象

DEBUG_UNCOLLECTABLE - 打印unreachable對(duì)象(除了uncollectable對(duì)象)

DEBUG_SAVEALL - 將對(duì)象保存到gc.garbage(一個(gè)列表)里面,而不是釋放它

DEBUG_LEAK - 對(duì)內(nèi)存泄漏的程序進(jìn)行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() # 強(qiáng)制觸發(fā)垃圾回收

# 下面都是自動(dòng)打印的

"""

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>, <__main__.b object at>, {'b': <__main__.b object at>}, {'a': <__main__.a object at>}]

17.6 總結(jié)

盡管python采用了最經(jīng)典的(最土的)的引用計(jì)數(shù)來作為自動(dòng)內(nèi)存管理的方案,但是python采用了多種方式來彌補(bǔ)引用計(jì)數(shù)的不足,內(nèi)存池的大量使用,標(biāo)記--清除(分代技術(shù)采用的去除循環(huán)引用的引用計(jì)數(shù)的方式)垃圾收集技術(shù)都極大地完善了python的內(nèi)存管理(包括申請(qǐng)、回收)機(jī)制。盡管引用計(jì)數(shù)機(jī)制需要花費(fèi)額外的開銷來維護(hù)引用計(jì)數(shù),但是現(xiàn)在這個(gè)年代,這點(diǎn)內(nèi)存算個(gè)啥。而且引用計(jì)數(shù)也有好處,不然早就隨著時(shí)代的前進(jìn)而被掃進(jìn)歷史的垃圾堆里面了。首先引用計(jì)數(shù)真的很方便,很直觀,對(duì)于很多對(duì)象引用計(jì)數(shù)能夠直接解決,不需要什么復(fù)雜的操作;另外引用計(jì)數(shù)將垃圾回收的開銷分?jǐn)傇诹苏麄€(gè)運(yùn)行時(shí),這對(duì)于python的響應(yīng)是有好處的。

當(dāng)然內(nèi)存管理和垃圾回收是一門給常精細(xì)和繁瑣的技術(shù),有興趣的話各位可以自己大刀闊斧的沖進(jìn)python的源碼中自由翱翔。

總結(jié)

以上是生活随笔為你收集整理的python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。