python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收...
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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跨计算机建立视图_计算机二级office
- 下一篇: pythonargmaxaxis1_ke