python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收...
17.0 序
內存管理,對于python這樣的動態語言是至關重要的一部分,它在很大程度上決定了python的執行效率,因為在python的運行中會創建和銷毀大量的對象,這些都設計內存的管理。同理python還提供了了內存的垃圾回收(GC,garbage collection),將開發者從繁瑣的手動維護內存的工作中解放出來。這一章我們就來分析python的GC是如何實現的。
17.1 內存管理架構
在python中內存管理機制是分層次的,我們可以看成有四層,0 1 2 3。在最底層,也就是第0層是由操作系統提供的內存管理接口,比如C提供了malloc和free接口,這一層是由操作系統實現并且管理的,python不能干涉這一行為。從這一層往上,剩余的三層則都是由python實現并維護的。
第一層是python基于第0層操作系統管理接口包裝而成的,這一層并沒有在第0層上加入太多的動作,其目的僅僅是為python提供一層統一的raw memory的管理接口。這么做的原因就是雖然不同的操作系統都提供了ANSI C標準所定義的內存管理接口,但是對于某些特殊情況不同操作系統有不同的行為。比如調用malloc(0),有的操作系統會返回NULL,表示申請失敗,但是有的操作系統則會返回一個貌似正常的指針, 但是這個指針指向的內存并不是有效的。為了最廣泛的可移植性,python必須保證相同的語義一定代表著相同的運行時行為,為了處理這些與平臺相關的內存分配行為,python必須要在C的內存分配接口之上再提供一層包裝。
在python中,第一層的實現就是一組以PyMem_為前綴的函數族,下面來看一下。
//include.h
PyAPI_FUNC(void *) PyMem_Malloc(size_t size);
PyAPI_FUNC(void *) PyMem_Realloc(void *ptr, size_t new_size);
PyAPI_FUNC(void) PyMem_Free(void *ptr);
//obmalloc.c
void *
PyMem_Malloc(size_t size)
{
/* see PyMem_RawMalloc() */
if (size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyMem.malloc(_PyMem.ctx, size);
}
void *
PyMem_Realloc(void *ptr, size_t new_size)
{
/* see PyMem_RawMalloc() */
if (new_size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyMem.realloc(_PyMem.ctx, ptr, new_size);
}
void
PyMem_Free(void *ptr)
{
_PyMem.free(_PyMem.ctx, ptr);
}
我們看到在第一層,python提供了類似于類似于C中malloc、realloc、free的語義。并且我們發現,比如PyMem_Malloc,如果申請的內存大小超過了PY_SSIZE_T_MAX直接返回NULL,并且調用了_PyMem.malloc,這個C中的malloc幾乎沒啥區別,但是會對特殊值進行一些處理。到目前為止,僅僅是分配了raw memory而已。其實在第一層,python還提供了面向對象中類型的內存分配器。
//pymem.h
#define PyMem_New(type, n) \
( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
( (type *) PyMem_Malloc((n) * sizeof(type)) ) )
#define PyMem_NEW(type, n) \
( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )
#define PyMem_Resize(p, type, n) \
( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
(type *) PyMem_Realloc((p), (n) * sizeof(type)) )
#define PyMem_RESIZE(p, type, n) \
( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
(type *) PyMem_REALLOC((p), (n) * sizeof(type)) )
#define PyMem_Del PyMem_Free
#define PyMem_DEL PyMem_FREE
很明顯,在PyMem_Malloc中需要程序員自行提供所申請的空間大小。然而在PyMem_New中,只需要提供類型和數量,python會自動偵測其所需的內存空間大小。
第一層所提供的內存管理接口的功能是非常有限的,如果創建一個PyLongObject對象,還需要做很多額外的工作,比如設置對象的類型參數、初始化對象的引用計數值等等。因此為了簡化python自身的開發,python在比第一層更高的抽象層次上提供了第二層內存管理接口。在這一層,是一組以PyObject_為前綴的函數族,主要提供了創建python對象的接口。這一套函數族又被換做Pymalloc機制。因此在第二層的內存管理機制上,python對于一些內建對象構建了更高抽象層次的內存管理策略。而對于第三層的內存管理策略,主要就是對象的緩存機制。因此:
第0層:操作系統負責管理內存,python無權干預
第1層:僅僅對c中原生的malloc進行了簡單包裝
第2層:真正在python中發揮巨大作用,并且也是GC的藏身之處
第3層:緩沖池,比如小整數對象池等等。
下面我們就來對第二層內存管理機制進行剖析。
17.2 小塊空間的內存池
在python中,很多時候申請的內存都是小塊的內存,這些小塊的內存在申請后很快又被釋放,并且這些內存的申請并不是為了創建對象,所以并沒有對象一級的內存池機制。這就意味著python在運行期間需要大量的執行malloc和free操作,導致操作系統在用戶態和內核態之間進行切換,這將嚴重影響python的效率。所以為了提高執行效率,python引入了一個內存池機制,用于管理對小塊內存的申請和釋放,這就是之前說的Pymalloc機制,并且提供了pymalloc_alloc,pymalloc_realloc,pymalloc_free三個接口。
整個小塊內存的內存池可以視為一個層次結構,在這個層次結構中一共分為4層,從下至上分別是:block、pool、arena和內存池。并且block(霧)、pool、arena都是python代碼中可以找到的實體,而最頂層的內存池只是一個概念上的東西,表示python對整個小塊內存分配和釋放行為的內存管理機制。
17.2.1 block
在最底層,block是一個確定大小的內存塊。而python中,有很多種block,不同種類的block都有不同的內存大小,這個內存大小的值被稱之為size?class。為了在當前主流的32位平臺和64位平臺都能獲得最佳性能,所有的block的長度都是8字節對齊的。
//obmalloc.c
#define ALIGNMENT 8 /* must be 2^N */
#define ALIGNMENT_SHIFT 3
同時,python為block的大小設定了一個上限,當申請的內存大小小于這個上限時,python可以使用不同種類的block滿足對內存的需求;當申請的內存大小超過了這個上限,python就會將對內存的請求轉交給第一層的內存管理機制,即PyMem函數族來處理。這個上限值在python中被設置為512,如果超過了這個值還是要經過操作系統臨時申請的。
//obmalloc.c
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
根據SMALL_REQUEST_THRESHOLD和ALIGNMENT的限定,實際上我們可以由此得到不同種類的block和size class。block是以8字節對齊,那么每一個塊的大小都是8的整倍數,最大不超過512
* Request in bytes Size of allocated block Size class idx
* ----------------------------------------------------------------
* 1-8 8 0
* 9-16 16 1
* 17-24 24 2
* 25-32 32 3
* 33-40 40 4
* 41-48 48 5
* 49-56 56 6
* 57-64 64 7
* 65-72 72 8
* ... ... ...
* 497-504 504 62
* 505-512 512 63
因此當我們申請一個44字節的內存時,PyObject_Malloc會從內存池中劃分一個48字節的block給我們。
另外在python中,block只是一個概念,在python源碼中沒有與之對應的實體存在。之前我們說對象,對象在源碼中有對應的PyObject,列表在源碼中則有對應的PyListObject,但是這里的block僅僅是概念上的東西,我們知道它是具有一定大小的內存,但是它并不與python源碼里面的某個東西對應。但是,python提供了一個管理block的東西,也就是我們下面要分析的pool。
17.2.2 pool
一組block的集合成為一個pool,換句話說,一個pool管理著一堆具有固定大小的內存塊(block)。事實上,pool管理者一大塊內存,它有一定的策略,將這塊大的內存劃分為多個小的內存塊。在python中,一個pool的大小通常是為一個系統內存頁,也就是4kb。
//obmalloc.c
#define SYSTEM_PAGE_SIZE (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK (SYSTEM_PAGE_SIZE - 1)
#define POOL_SIZE SYSTEM_PAGE_SIZE /* must be 2^N */
#define POOL_SIZE_MASK SYSTEM_PAGE_SIZE_MASK
雖然python沒有為block提供對應的結構,但是提供了和pool相關的結構,我們來看看
//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* 當然pool里面的block數量 */
block *freeblock; /* 一個鏈表,指向下一個可用的block */
struct pool_header *nextpool; /* 指向下一個pool */
struct pool_header *prevpool; /* 指向上一個pool "" */
uint arenaindex; /* 在area里面的索引 */
uint szidx; /* block的大小(固定值?后面說) */
uint nextoffset; /* 下一個可用block的內存偏移量 */
uint maxnextoffset; /* 最后一個block距離開始位置的距離 */
};
typedef struct pool_header *poolp;
我們剛才說了一個pool的大小在python中是4KB,但是從當前的這個pool的結構體來看,用鼻子想也知道吃不完4KB的內存。所以呀,這個結構體叫做pool_header,它僅僅一個pool的頭部,除去這個pool_header,還剩下的內存才是維護的所有block的集合所占的內存。
我們注意到,pool_header里面有一個szidx,這就意味著pool里面管理的內存塊大小都是一樣的。也就是說,一個pool可能管理了20個32字節的block、也可能管理了20個64字節的block,但是不會出現管理了10個32字節的block加上10個64字節的block存在。每一個pool都和一個size聯系在一起,更確切的說都和一個size class index聯系在一起,表示pool里面存儲的block都是多少字節的。這就是里面的域szidx存在的意義。
假設我們手里有一塊4kb的內存,來看看python是如何將這塊內存改造為一個管理32字節block的pool,并從中取出第一塊pool的。
//obmalloc.c
#define POOL_OVERHEAD _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
block *bp;
poolp pool;
...
//pool指向了一塊4kb的內存
init_pool:
pool->ref.count = 1;
...
//設置pool的size class index
pool->szidx = size;
//將size class index轉換成size,比如:0->8, 1->16, 63->512
size = INDEX2SIZE(size);
//跳過用于pool_header的內存,并進行對齊
bp = (block *)pool + POOL_OVERHEAD;
//等價于pool->nextoffset = POOL_OVERHEAD+size+size
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
goto success;
...
success:
UNLOCK();
assert(bp != NULL);
*ptr_p = (void *)bp;
return 1;
}
最后的(void *)bp;就是指向從pool中取出的第一塊block的指針。也就是說pool中第一塊block已經被分配了,所以在ref.count中記錄了當前已經被分配的block的數量,這時為1,特別需要注意的是,bp返回的實際上是一個地址,這個地址之后有將近4kb的內存實際上都是可用的,但是可以肯定申請內存的函數只會使用[bp, bp+size]這個區間的內存,這是由size?class index可以保證的。改造成pool之后的4kb內存如圖所示:
實線箭頭是指針,但是虛線箭頭則是偏移位置的形象表示。在nextoffset,maxnextoffset中存儲的是相對于pool頭部的偏移位置。
在了解初始化之后的pool的樣子之后,可以來看看python在申請block時,pool_header中的各個域是怎么變動的。假設我們從現在開始連續申請5塊28字節內存,由于28字節對應的size class index為3,所以實際上會申請5塊32字節的內存。
//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
if (pool != pool->nextpool) {
/*
* There is a used pool for this size class.
* Pick up the head block of its free list.
*/
//首先pool中block數自增1
++pool->ref.count;
//這里的freeblock指向的是下一個可用的block的起始地址
bp = pool->freeblock;
assert(bp != NULL);
if ((pool->freeblock = *(block **)bp) != NULL) {
goto success;
}
//因此當再次申請32字節block時,只需要返回freeblock指向的地址就可以了。那么很顯然,freeblock需要前進,指向下一個可用的block,這個時候nextoffset就現身了
if (pool->nextoffset <= pool->maxnextoffset) {
//當nextoffset小于等于maxoffset時候
//freeblock等于當前block的地址 + nextoffset(下一個可用block的內存偏移量)
//所以freeblock正好指向了下一個可用block的地址
pool->freeblock = (block*)pool +
pool->nextoffset;
//同理,nextoffset也要向前移動一個block的距離
pool->nextoffset += INDEX2SIZE(size);
//依次反復,即可對所有的block進行遍歷。而maxnextoffset指明了該pool中最后一個可用的block距離pool開始位置的偏移
//當pool->nextoffset > pool->maxnextoffset就意味著遍歷完pool中的所有block了
//再次獲取顯然就是NULL了
*(block **)(pool->freeblock) = NULL;
goto success;
}
}
所以,申請、前進、申請、前進,一直重復著相同的動作,整個過程非常自然,也容易理解。但是我們發現,由于無論多少個block,這些block必須都是具有相同大小,導致一個pool中只能滿足POOL_SIZE?/?size次對block的申請,這就讓人不舒服。舉個栗子,現在我們已經進行了5次連續32字節的內存分配,可以想象,pool中5個連續的block都被分配出去了。過了一段時間,程序釋放了其中的第2塊和第4塊block,那么下一次再分配32字節的內存的時候,pool提交的應該是第2塊,還是第6塊呢?顯然為了pool的使用效率,最好分配自由的第二塊block。因此可以想象,一旦python運轉起來,內存的釋放動作將導致pool中出現大量的離散的自由block,python為了知道哪些block是被使用之后再次被釋放的,必須建立一種機制,將這些離散自由的block組合起來,再次使用。這個機制就是所有的自由block鏈表,這個鏈表的關鍵就在pool_header中的那個freeblock身上。
//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* 當然pool里面的block數量 */
block *freeblock; /* 一個鏈表,指向下一個可用的block */
struct pool_header *nextpool; /* 指向下一個pool */
struct pool_header *prevpool; /* 指向上一個pool "" */
uint arenaindex; /* 在area里面的索引 */
uint szidx; /* block的大小(固定值?后面說) */
uint nextoffset; /* 下一個可用block的內存偏移量 */
uint maxnextoffset; /* 最后一個block距離開始位置的距離 */
};
typedef struct pool_header *poolp;
剛才我們說了,當pool初始化完后之后,freeblock指向了一個有效的地址,也就是下一個可以分配出去的block的地址。然而奇特的是,當python設置了freeblock時,還設置了*freeblock。這個動作看似詭異,然而我們馬上就能看到設置*freeblock的動作正是建立離散自由block鏈表的關鍵所在。目前我們看到的freeblock只是在機械地前進前進,因為它在等待一個特殊的時刻,在這個特殊的時刻,你會發現freeblock開始成為一個蘇醒的精靈,在這4kb的內存上開始靈活地舞動。這個特殊的時刻就是一個block被釋放的時刻。
//obmalloc.c
//基于地址P獲得離P最近的pool的邊界地址
#define POOL_ADDR(P) ((poolp)_Py_ALIGN_DOWN((P), POOL_SIZE))
static int
pymalloc_free(void *ctx, void *p)
{
poolp pool;
block *lastfree;
poolp next, prev;
uint size;
pool = POOL_ADDR(p);
//如果p不在pool里面,直接返回0
if (!address_in_range(p, pool)) {
return 0;
}
LOCK();
//釋放,那么ref.count就是勢必大于0
assert(pool->ref.count > 0); /* else it was empty */
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
}
在釋放block時,神秘的freeblock驚鴻一現,覆蓋在freeblock身上的那層面紗就要被揭開了。我們知道,這是freeblock雖然指向了一個有效的pool里面的地址,但是*freeblock是為NULL的。假設這時候python釋放的是block A,那么A中的第一個字節的值被設置成了當前freeblock的值,然后freeblock的值被更新了,指向了block A的首地址。就是這兩個步驟,一個block被插入到了離散自由的block鏈表中,所以當第2塊和第4塊block都被釋放之后,我們可以看到一個初具規模的離散自由block鏈表了。
到了這里,這條實現方式非常奇特的block鏈表被我們挖掘出來了,從freeblock開始,我們可以很容易的以freeblock?=?*freeblock的方式遍歷這條鏈表,而當發現了*freeblock為NULL時,則表明到達了該鏈表(可用自由鏈表)的尾部了,那么下次就需要申請新的block了。
//obmalloc.c
static int
pymalloc_alloc(void *ctx, void *p)
{
if (pool != pool->nextpool) {
++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
//如果這里的條件不為真,表明離散自由鏈表中已經不存在可用的block了
//如果可能,則會繼續分配pool的nextoffset指定的下一塊block
if ((pool->freeblock = *(block **)bp) != NULL) {
goto success;
}
/*
* Reached the end of the free list, try to extend it.
*/
if (pool->nextoffset <= pool->maxnextoffset) {
...
}
}
但是如果連pool->nextoffset <= pool->maxnextoffset這個條件都不成立了呢?pool的大小有限制啊,如果我再想申請block的時候,沒空間了怎么辦?再來一個pool不就好了,所以多個block可以組合成一個集合,pool;那么多個pool也可以組合起來,就是我們下面介紹的arena。
17.2.3 arena
在python中,多個pool聚合的結果就是一個arena。上一節提到,pool的大小默認是4kb,同樣每個arena的大小也有一個默認值。#define ARENA_SIZE (256 << 10),顯然這個值默認是256KB,也就是ARENA_SIZE?/?POOL_SIZE?=?64個pool的大小。
//obmalloc.c
struct arena_object {
uintptr_t address;
block* pool_address;
uint nfreepools;
uint ntotalpools;
struct pool_header* freepools;
struct arena_object* nextarena;
struct arena_object* prevarena;
};
一個概念上的arena在python源碼中就對應arena_object結構體,確切的說,arena_object僅僅是arena的一部分。就像pool_header僅僅是pool的一部分一樣,一個完整的pool包括一個pool_header和透過這個pool_header管理著的block集合;一個完整的arena也包括一個arena_object和透過這個arena_object管理著的pool集合。
"未使用的"的arena和"可用"的arena
在arena_object結構體的定義中,我們看到了nextarena和prevarena這兩個東西,這似乎意味著在python中會有一個或多個arena構成的鏈表,這個鏈表的表頭就是arenas。呃,這種猜測實際上只對了一半,實際上,在python中確實會存在多個arena_object構成的集合,但是這個集合不夠成鏈表,而是一個數組。數組的首地址由arenas來維護,這個數組就是python中的通用小塊內存的內存池。另一方面,nextarea和prevarena也確實是用來連接arena_object組成鏈表的,咦,不是已經構成或數組了嗎?為啥又要來一個鏈表。
我們曾說arena是用來管理一組pool的集合的,arena_object的作用看上去和pool_header的作用是一樣的。但是實際上,pool_header管理的內存和arena_object管理的內存有一點細微的差別。pool_header管理的內存pool_header自身是一塊連續的內存,但是arena_object與其管理的內存則是分離的:
咋一看,貌似沒啥區別,不過一個是連著的,一個是分開的。但是這后面隱藏了這樣一個事實:當pool_header被申請時,它所管理的內存也一定被申請了;但是當arena_object被申請時,它所管理的pool集合的內存則沒有被申請。換句話說,arena_object和pool集合在某一時刻需要建立聯系。
當一個arena的arena_object沒有與pool建立聯系的時候,這時的arena就處于"未使用"狀態;一旦建立了聯系,這時arena就轉換到了"可用"狀態。對于每一種狀態,都有一個arena鏈表。"未使用"的arena鏈表表頭是unused_arena_objects,多個arena之間通過nextarena連接,并且是一個單向的鏈表;而"可用的"arena鏈表表頭是usable_arenas,多個arena之間通過nextarena、prevarena連接,是一個雙向鏈表。
申請arena
在運行期間,python使用new_arena來創建一個arena,我們來看看它是如何被創建的。
//obmalloc.c
//arenas,多個arena組成的數組的首地址
static struct arena_object* arenas = NULL;
//當arena數組中的所有arena的個數
static uint maxarenas = 0;
//未使用的arena的個數
static struct arena_object* unused_arena_objects = NULL;
//可用的arena的個數
static struct arena_object* usable_arenas = NULL;
//初始化需要申請的arena的個數
#define INITIAL_ARENA_OBJECTS 16
static struct arena_object*
new_arena(void)
{
//arena,一個arena_object結構體對象
struct arena_object* arenaobj;
uint excess; /* number of bytes above pool alignment */
//[1]:判斷是否需要擴充"未使用"的arena列表
if (unused_arena_objects == NULL) {
uint i;
uint numarenas;
size_t nbytes;
//[2]:確定本次需要申請的arena_object的個數,并申請內存
numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
...
nbytes = numarenas * sizeof(*arenas);
arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
if (arenaobj == NULL)
return NULL;
arenas = arenaobj;
...
/* Put the new arenas on the unused_arena_objects list. */
//[3]:初始化新申請的arena_object,并將其放入"未使用"arena鏈表中
for (i = maxarenas; i < numarenas; ++i) {
arenas[i].address = 0; /* mark as unassociated */
arenas[i].nextarena = i < numarenas - 1 ?
&arenas[i+1] : NULL;
}
/* Update globals. */
unused_arena_objects = &arenas[maxarenas];
maxarenas = numarenas;
}
/* Take the next available arena object off the head of the list. */
//[4]:從"未使用"arena鏈表中取出一個"未使用"的arena
assert(unused_arena_objects != NULL);
arenaobj = unused_arena_objects;
unused_arena_objects = arenaobj->nextarena;
assert(arenaobj->address == 0);
//[5]:申請arena管理的內存,這里我們說的arena指的是arena_object,簡寫了
address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
if (address == NULL) {
/* The allocation failed: return NULL after putting the
* arenaobj back.
*/
arenaobj->nextarena = unused_arena_objects;
unused_arena_objects = arenaobj;
return NULL;
}
arenaobj->address = (uintptr_t)address;
//調整個數
++narenas_currently_allocated;
++ntimes_arena_allocated;
if (narenas_currently_allocated > narenas_highwater)
narenas_highwater = narenas_currently_allocated;
//[6]:設置poo集合的相關信息,這是設置為NULL
arenaobj->freepools = NULL;
/* pool_address
nfreepools
arenaobj->pool_address = (block*)arenaobj->address;
arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);
//將pool的起始地址調整為系統頁的邊界
excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
if (excess != 0) {
--arenaobj->nfreepools;
arenaobj->pool_address += POOL_SIZE - excess;
}
arenaobj->ntotalpools = arenaobj->nfreepools;
return arenaobj;
}
因此我們可以看到,python首先會檢查當前"未使用"鏈表中是否還有"未使用"arena,檢查的結果將決定后續的動作。
如果在"未使用"鏈表中還存在未使用的arena,那么python會從"未使用"arena鏈表中抽取一個arena,接著調整"未使用"鏈表,讓它和抽取的arena斷絕一切聯系。然后python申請了一塊256KB大小的內存,將申請的內存地址賦給抽取出來的arena的address。我們已經知道,arena中維護的是pool集合,這塊256KB的內存就是pool的容身之處,這時候arena就已經和pool集合建立聯系了。這個arena已經具備了成為"可用"內存的條件,該arena和"未使用"arena鏈表脫離了關系,就等著被"可用"arena鏈表接收了,不過什么時候接收呢?先別急
隨后,python在代碼的[6]處設置了一些arena用戶維護pool集合的信息。需要注意的是,python將申請到的256KB內存進行了處理,主要是放棄了一些內存,并將可使用的內存邊界(pool_address)調整到了與系統頁對齊。然后通過arenaobj->freepools = NULL;將freepools設置為NULL,這不奇怪,基于對freeblock的了解,我們知道要等到釋放一個pool時,這個freepools才會有用。最后我們看到,pool集合占用的256KB內存在進行邊界對齊后,實際是交給pool_address來維護了。
回到new_arena中的[1]處,如果unused_arena_objects為NULL,則表明目前系統中已經沒有"未使用"arena了,那么python首先會擴大系統的arena集合(小塊內存內存池)。python在內部通過一個maxarenas的變量維護了存儲arena的數組的個數,然后在[2]處將待申請的arena的個數設置為當然arena個數(maxarenas)的2倍。當然首次初始化的時候maxarenas為0,此時為16。
在獲得了新的maxarenas后,python會檢查這個新得到的值是否溢出了。如果檢查順利通過,python就會在[3]處通過realloc擴大arenas指向的內存,并對新申請的arena_object進行設置,特別是那個不起眼的address,要將新申請的address一律設置為0。實際上,這是一個標識arena是出于"未使用"狀態還是"可用"狀態的重要標記。而一旦arena(arena_object)和pool集合建立了聯系,這個address就變成了非0,看代碼的[6]處。當然別忘記我們為什么會走到[3]這里,是因為unused_arena_objects == NULL了,而且最后還設置了unused_arena_objects,這樣系統中又有了"未使用"的arena了,接下來python就在[4]處對一個arena進行初始化了。
17.2.4 內存池
可用pool緩沖池--usedpools
通過#define SMALL_REQUEST_THRESHOLD 512我們知道python內部默認的小塊內存與大塊內存的分界點定在512個字節。也就是說,當申請的內存小于512個字節,pymalloc_alloc會在內存池中申請內存,而當申請的內存超過了512字節,那么pymalloc_alloc將退化為malloc,通過操作系統來申請內存。當然,通過修改python源代碼我們可以改變這個值,從而改變python的默認內存管理行為。
當申請的內存小于512字節時,python會使用area所維護的內存空間。那么python內部對于area的個數是否有限制呢?換句話說,python對于這個小塊空間內存池的大小是否有限制?其實這個決策取決于用戶,python提供了一個編譯符號,用于控制是否限制內存池的大小,不過這里不是重點,只需要知道就行。
盡管我們在前面花了不少篇幅介紹arena,同時也看到arena是python的小塊內存池的最上層結構,所有arena的集合實際就是小塊內存池。然而在實際的使用中,python并不直接與arenas和arena數組打交道。當python申請內存時,最基本的操作單元并不是arena,而是pool。估計到這里懵了,別急,慢慢來。
舉個例子,當我們申請一個28字節的內存時,python內部會在內存池尋找一塊能夠滿足需求的pool,從中取出一個block返回,而不會去尋找arena。這實際上是由pool和arena的屬性決定的,在python中,pool是一個有size概念的內存管理抽象體,一個pool中的block總是有確定的大小,這個pool總是和某個size class index對應,還記得pool_header中的那個szidx么?而arena是沒有size概念的內存管理抽象體。這就意味著,同一個arena在某個時刻,其內部的pool集合可能都是32字節的block;而到了另一個時刻,由于系統需要,這個arena可能被重新劃分,其中的pool集合可能改為64字節的block了,甚至pool集合中一般的pool管理32字節,另一半管理64字節。這就決定了在進行內存分配和銷毀時,所有的動作都是在pool上完成的。
當然內存池中的pool不僅僅是一個有size概念的內存管理抽象體,更進一步的,它還是一個有狀態的內存管理抽象體。一個pool在python運行的任何一個時刻,總是處于一下三種狀態中的一種:
used狀態:pool中至少有一個block已經被使用,并且至少有一個block未被使用。這種狀態的pool受控于python內部維護的usedpools數組。
full狀態:pool中所有的block都已經被使用,這種狀態的pool在arena中,但是不再arena的freepools鏈表中。
empty狀態:pool中所有的block都未被使用,處于這個狀態的pool的集合通過其pool_header中的nextpool構成一個鏈表,這個鏈表的表頭就是arena中的freepools。
請注意:arena中處于full狀態的pool是各自獨立,沒有像其他狀態的pool一樣,連接成一個鏈表。
我們從圖中看到所有的處于used狀態的pool都被置于usedpools的控制之下。python內部維護的usedpools數組是一個非常巧妙的實現,維護著所有的處于used狀態的pool。當申請內存時,python就會通過usedpools尋找到一個可用的pool(處于used狀態),從中分配一個block。因此我們想,一定有一個usedpools相關聯的機制,完成從申請的內存的大小到size class index之間的轉換,否則python就無法找到最合適的pool了。這種機制和usedpools的結構有著密切的關系,我們看一下它的結構。
//obmalloc.c
typedef uint8_t block;
#define PTA(x) ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x) PTA(x), PTA(x)
//NB_SMALL_SIZE_CLASSES之前好像出現過,但是不用說也知道這表示當前配置下有多少個不同size的塊
//在我當前的機器就是512/8=64個,對應的size class index就是從0到63
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8
, PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
#if NB_SMALL_SIZE_CLASSES > 16
, PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
#if NB_SMALL_SIZE_CLASSES > 24
, PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
#if NB_SMALL_SIZE_CLASSES > 32
, PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
#if NB_SMALL_SIZE_CLASSES > 40
, PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
#if NB_SMALL_SIZE_CLASSES > 48
, PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
#if NB_SMALL_SIZE_CLASSES > 56
, PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
#if NB_SMALL_SIZE_CLASSES > 64
#error "NB_SMALL_SIZE_CLASSES should be less than 64"
#endif /* NB_SMALL_SIZE_CLASSES > 64 */
#endif /* NB_SMALL_SIZE_CLASSES > 56 */
#endif /* NB_SMALL_SIZE_CLASSES > 48 */
#endif /* NB_SMALL_SIZE_CLASSES > 40 */
#endif /* NB_SMALL_SIZE_CLASSES > 32 */
#endif /* NB_SMALL_SIZE_CLASSES > 24 */
#endif /* NB_SMALL_SIZE_CLASSES > 16 */
#endif /* NB_SMALL_SIZE_CLASSES > 8 */
};
感覺這個數組有點怪異,別急我們來畫圖看一看
考慮一下當申請28字節的情形,前面我們說到,python首先會獲得size class index,顯然這里是3。那么在usedpools中,尋找第3+3=6個元素,發現usedpools[6]的值是指向usedpools[4]的地址。好暈啊,好吧,現在對照pool_header的定義來看一看usedpools[6]?->?nextpool這個指針指向哪里了呢?
//obmalloc.c
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* 當然pool里面的block數量 */
block *freeblock; /* 一個鏈表,指向下一個可用的block */
struct pool_header *nextpool; /* 指向下一個pool */
struct pool_header *prevpool; /* 指向上一個pool "" */
uint arenaindex; /* 在area里面的索引 */
uint szidx; /* block的大小(固定值?后面說) */
uint nextoffset; /* 下一個可用block的內存偏移量 */
uint maxnextoffset; /* 最后一個block距離開始位置的距離 */
};
顯然是從usedpools[6](即usedpools+4)開始向后偏移8個字節(一個ref的大小加上一個freeblock的大小)后的內存,正好是usedpools[6]的地址(即usedpools+6),這是python內部的trick
想象一下,當我們手中有一個size class為32字節的pool,想要將其放入這個usedpools中時,要怎么做呢?從上面的描述我們知道,只需要進行usedpools[i+i]?->?nextpool?=?pool即可,其中i為size class index,對應于32字節,這個i為3.當下次需要訪問size class 為32字節(size class index為3)的pool時,只需要簡單地訪問usedpools[3+3]就可以得到了。python正是使用這個usedpools快速地從眾多的pool中快速地尋找到一個最適合當前內存需求的pool,從中分配一塊block。
//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
block *bp;
poolp pool;
poolp next;
uint size;
...
LOCK();
//獲得size class index
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
//直接通過usedpools[size+size],這里的size不就是我們上面說的i嗎?
pool = usedpools[size + size];
//如果usedpools中有可用的pool
if (pool != pool->nextpool) {
... //有可用pool
}
... //無可用pool,嘗試獲取empty狀態的pool
}
Pool的初始化
當python啟動之后,在usedpools這個小塊空間的內存池中,并不存在任何可用的內存,準確的說,不存在任何可用的pool。在這里,python采用了延遲分配的策略,即當我們確實開始申請小塊內存的時候,python才建立這個內存池。正如之前提到的,當我們開始申請28字節的內存時,python實際將申請32字節的內存,然后會首先根據32字節對應的class size index(3)在usedpools中對應的位置查找,如果發現在對應的位置后面沒有連接任何可用的pool,python會從"可用"arena鏈表中的第一個可用的arena中獲取的一個pool。不過需要注意的是,當前獲得的arena中包含的這些pools中可能會具有不同的class size index。
想象一下,當申請32字節的內存時,從"可用"arena中取出一個pool用作32字節的pool。當下一次內存分配請求分配64字節的內存時,python可以直接使用當前"可用"的arena的另一個pool即可,正如我們之前說的arena沒有size class的屬性,而pool才有。
//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
block *bp;
poolp pool;
poolp next;
uint size;
...
LOCK();
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
pool = usedpools[size + size];
//如果usedpools中有可用的pool
if (pool != pool->nextpool) {
... //有可用pool
}
//無可用pool,嘗試獲取empty狀態的pool
if (usable_arenas == NULL) {
//嘗試申請新的arena,并放入"可用"arena鏈表
usable_arenas = new_arena();
if (usable_arenas == NULL) {
goto failed;
}
usable_arenas->nextarena =
usable_arenas->prevarena = NULL;
}
assert(usable_arenas->address != 0);
//從可用arena鏈表中第一個arena的freepools中抽取一個可用的pool
pool = usable_arenas->freepools;
if (pool != NULL) {
/* Unlink from cached pools. */
usable_arenas->freepools = pool->nextpool;
//調整可用arena鏈表中第一個arena中的可用pool的數量
--usable_arenas->nfreepools;
//如果調整之后變為0,則將該arena從可用arena鏈表中移除
if (usable_arenas->nfreepools == 0) {
/* Wholly allocated: remove. */
assert(usable_arenas->freepools == NULL);
assert(usable_arenas->nextarena == NULL ||
usable_arenas->nextarena->prevarena ==
usable_arenas);
usable_arenas = usable_arenas->nextarena;
if (usable_arenas != NULL) {
usable_arenas->prevarena = NULL;
assert(usable_arenas->address != 0);
}
}
else {
/* nfreepools > 0: it must be that freepools
* isn't NULL, or that we haven't yet carved
* off all the arena's pools for the first
* time.
*/
assert(usable_arenas->freepools != NULL ||
usable_arenas->pool_address <=
(block*)usable_arenas->address +
ARENA_SIZE - POOL_SIZE);
}
init_pool:
...
}
可以看到,如果開始時"可用"arena鏈表為空,那么python會通過new_arena申請一個arena,開始構建"可用"arena鏈表。還記得我們之前遺留了一個問題嗎?答案就在這里。在這里,一個脫離了"未使用"arena鏈表并轉變為"可用"的arena被納入了"可用"arena鏈表的控制。所以python會嘗試從"可用"arena鏈表中的第一個arena所維護的pool集合中取出一個可用的pool。如果成功地取出了這個pool,那么python就會進行一些維護信息的更新工作,甚至在當前arena中可用的pool已經用完了之后,將該arena從"可用"arena鏈表中移除
好了,現在我們手里有了一塊用于32字節內存分配的pool,為了提高以后內存分配的效率,我們需要將這個pool放入到usedpools中。這一步就是我們上面代碼中沒貼的init
//obmalloc.c
static int
pymalloc_alloc(void *ctx, void **ptr_p, size_t nbytes)
{
init_pool:
//將pool放入usedpools中
next = usedpools[size + size]; /* == prev */
pool->nextpool = next;
pool->prevpool = next;
next->nextpool = pool;
next->prevpool = pool;
pool->ref.count = 1;
//pool在之前就具有正確的size結構,直接返回pool中的一個block
if (pool->szidx == size) {
bp = pool->freeblock;
assert(bp != NULL);
pool->freeblock = *(block **)bp;
goto success;
}
//pool之前就具有正確的size結果,直接返回pool中的一個block
pool->szidx = size;
size = INDEX2SIZE(size);
bp = (block *)pool + POOL_OVERHEAD;
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
goto success;
}
}
具體的細節可以自己觀察源代碼去研究,這里不再寫了,有點累
block的釋放
考察完了對block的分配,是時候來看看對block的釋放了。對block的釋放實際上就是將一塊block歸還給pool,我們已經知道pool可能存在3種狀態,在分別處于三種狀態,它們的位置是各不相同的。
當我們釋放一個block之后,可能會引起pool狀態的轉變,這種轉變可分為兩種情況
used狀態轉變為empty狀態
full狀態轉變為used狀態
//obmalloc.c
static int
pymalloc_free(void *ctx, void *p)
{
poolp pool;
block *lastfree;
poolp next, prev;
uint size;
pool = POOL_ADDR(p);
if (!address_in_range(p, pool)) {
return 0;
}
LOCK();
assert(pool->ref.count > 0); /* else it was empty */
//設置離散自由的block鏈表
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
//如果!lastfree成立,那么意味著不存在lastfree,說明這個pool在釋放block之前是滿的
if (!lastfree) {
/* Pool was full, so doesn't currently live in any list:
* link it to the front of the appropriate usedpools[] list.
* This mimics LRU pool usage for new allocations and
* targets optimal filling when several pools contain
* blocks of the same size class.
*/
//當前pool處于full狀態,在釋放一塊block之后,需要將其轉換為used狀態
//并重新鏈入到usedpools的頭部
--pool->ref.count;
assert(pool->ref.count > 0); /* else the pool is empty */
size = pool->szidx;
next = usedpools[size + size];
prev = next->prevpool;
pool->nextpool = next;
pool->prevpool = prev;
next->prevpool = pool;
prev->nextpool = pool;
goto success;
}
struct arena_object* ao;
uint nf; /* ao->nfreepools */
//否則到這一步表示lastfree有效
//pool回收了一個block之后,不需要從used狀態轉換為empty狀態
if (--pool->ref.count != 0) {
/* pool isn't empty: leave it in usedpools */
goto success;
}
/* Pool is now empty: unlink from usedpools, and
* link to the front of freepools. This ensures that
* previously freed pools will be allocated later
* (being not referenced, they are perhaps paged out).
*/
//否則說明pool為空
next = pool->nextpool;
prev = pool->prevpool;
next->prevpool = prev;
prev->nextpool = next;
//將pool放入freepools維護的鏈表中
ao = &arenas[pool->arenaindex];
pool->nextpool = ao->freepools;
ao->freepools = pool;
nf = ++ao->nfreepools;
if (nf == ao->ntotalpools) {
//調整usable_arenas鏈表
if (ao->prevarena == NULL) {
usable_arenas = ao->nextarena;
assert(usable_arenas == NULL ||
usable_arenas->address != 0);
}
else {
assert(ao->prevarena->nextarena == ao);
ao->prevarena->nextarena =
ao->nextarena;
}
/* Fix the pointer in the nextarena. */
if (ao->nextarena != NULL) {
assert(ao->nextarena->prevarena == ao);
ao->nextarena->prevarena =
ao->prevarena;
}
//調整"未使用"arena鏈表
ao->nextarena = unused_arena_objects;
unused_arena_objects = ao;
//程序走到這一步,表示是pool原先是used,釋放block之后依舊是used
//那么會將內存歸還給操作系統
_PyObject_Arena.free(_PyObject_Arena.ctx,
(void *)ao->address, ARENA_SIZE);
//設置address,將arena的狀態轉為"未使用"
ao->address = 0; /* mark unassociated */
--narenas_currently_allocated;
goto success;
}
}
實際上在python2.4之前,python的arena是不會釋放pool的。這樣的話就會引起內存泄漏,比如我們申請10?*?1024?*?1024個16字節的小內存,這就意味著必須使用160MB的內存,由于python會默認全部使用arena(這一點我們沒有提)來滿足你的需求。但是當我們將所有16字節的內存全部釋放了,這些內存也會回到arena的控制之中,這都沒有問題。但是問題來了,這些內存是被arena控制的,并沒有交給操作系統啊,,所以這160MB的內存始終會被python占用,如果后面程序再也不需要160MB如此巨大的內存,那么不就浪費了嗎?
由于這種情況必須在大量持續申請小內存對象時才會出現,因為大的話會自動交給操作系統了,小的才會由arena控制,而持續申請大量小內存的情況幾乎不會碰到,所以這個問題也就留在了 Python中。但是因為有些人發現了這個問題,所以這個問題在python2.5的時候就得到了解決。
因為早期的python,arena是沒有區分"未使用"和"可用"兩種狀態的,到了python2.5中,arena已經可以將自己維護的pool集合釋放,交給操作系統了,從而將"可用"狀態轉化為"未使用"狀態。而當python處理完pool,就開始處理arena了。
而對arena的處理實際上分為了4中情況
1.如果arena中所有的pool都是empty的,釋放pool集合所占用的內存
2.如果之前arena中沒有了empty的pool,那么在"可用"鏈表中就找不到該arena,由于現在arena中有了一個pool,所以需要將這個arena鏈入到"可用"鏈表的表頭
3.如果arena中的empty的pool的個數為n,那么會從"可用"arena鏈表中開始尋找arena可以插入的位置,將arena插入到"可用"鏈表。這樣操作的原因就在于"可用"arena鏈表實際上是一個有序的鏈表,從表頭開始往后,每一個arena中empty的pool的個數,即nfreepools,都不能大于前面的arena,也不能小于后面的arena。保持這樣有序性的原則是分配block時,是從"可用"鏈表的表頭開始尋找可用arena的,這樣就能保證如果一個arena的empty pool數量越多,它被使用的機會就越少。因此它最終釋放其維護的pool集合的內存的機會就越大,這樣就能保證多余的內存會被歸還給操作系統
4.其他情況,則不對arena進行任何處理。
內存池全景
前面我們已經提到了,對于一個用c開發的龐大的軟件(python是一門高級語言,但是執行對應代碼的解釋器則可以看成是c的一個軟件),其中的內存管理可謂是最復雜、最繁瑣的地方了。不同尺度的內存會有不同的抽象,這些抽象在各種情況下會組成各式各樣的鏈表,非常復雜。但是我們還是有可能從一個整體的尺度上把握整個內存池,盡管不同的鏈表變幻無常,但我們只需記住,所有的內存都在arenas(或者說那個存放多個arena的數組)的掌握之中 。
17.3 循環引用之垃圾回收
17.3.1 引用計數之垃圾回收
現在絕大部分語言都實現了垃圾回收機制,也包括python。然而python的垃圾回收和java,c#等語言有一個很大的不同,那就是python中大多數對象的生命周期是通過對象的引用計數來管理的,這一點在開始的章節我們就說了,對于python中最基礎的對象PyObject,有兩個屬性,一個是該對象的類型,還有一個就是引用計數(ob_refcnt)。不過從廣義上將,引用計數也算是一種垃圾回收機制,而且它是一中最簡單最直觀的垃圾回收計數。盡管需要一個值來維護引用計數,但是引用計數有一個最大的優點:實時性。任何內存,一旦沒有指向它的引用,那么就會被回收。而其他的垃圾回收技術必須在某種特定條件下(比如內存分配失敗)才能進行無效內存的回收。
引用計數機制所帶來的維護引用計數的額外操作,與python運行中所進行的內存分配、釋放、引用賦值的次數是成正比的。這一點,相對于主流的垃圾回收技術,比如標記--清除(mark--sweep)、停止--復制(stop--copy)等方法相比是一個弱點,因為它們帶來額外操作只和內存數量有關,至于多少人引用了這塊內存則不關心。因此為了與引用計數搭配、在內存的分配和釋放上獲得最高的效率,python設計了大量的內存池機制,比如小整數對象池、字符串的intern機制,列表的freelist緩沖池等等,這些大量使用的面向特定對象的內存池機制正是為了彌補引用計數的軟肋。
其實對于現在的cpu和內存來說,上面的問題都不是什么問題。但是引用計數還存在一個致命的缺陷,這一缺陷幾乎將引用計數機制在垃圾回收技術中判處了"死刑",這一技術就是"循環引用"。而且也正是因為"循環引用"這個致命傷,導致在狹義上并不把引用計數機制看成是垃圾回收技術
在介紹循環引用之前,先來看看python引用計數什么時候會增加,什么時候會減少。
引用計數加一
對象被創建:a=1
對象被引用:b=a
對象被作為參數傳到一個函數中,func(a)
對象作為列表、元組等其他容器里面的元素
引用計數減一
對象別名被顯式的銷毀:del a
對象的引用指向了其他的對象:a=2
對象離開了它的作用域,比如函數的局部變量,在函數執行完畢的時候,也會被銷毀(如果沒有獲取棧幀的話),而全局變量則不會
對象所在的容器被銷毀,或者從容器中刪除等等
查看引用計數
查看一個對象的引用計數,可以通過sys.getrefcount(obj),但是由于作為getrefcount這個函數的參數,所以引用計數會多1。
我們之前說,a =?"mashiro",相當于把a和a對應的值組合起來放在了命名空間里面,那么你認為這個a對應的值是什么呢?難道是"mashiro"這個字符串嗎?其實從python的層面上來看的話確實是這樣,但是在python的底層,其實存儲的是字符數組"mashiro"對應地址,我總覺得前面章節好像說錯了。
b=a在底層中則表示把a的指針拷貝給了b,是的你沒有看錯,都說python傳遞的是符號,但是在底層就是傳遞了一個指針,無論什么傳遞的都是指針,在python的層面上傳遞就是符號、或者就是引用。所以我們看到, 每當多了一個引用,那么"mashiro"(在c的層面上是一個結構體,PyUnicodeObject)的引用計數就會加1.
而每當減少一個引用,引用計數就會減少1。盡管我們用sys.getrefcount得到的結果是2,但是當這個函數執行完,由于局部變量的銷毀,其實結果已經變成了1。因此引用計數很方便,就是當一片空間沒有人引用了,那么就直接銷毀。盡管維護這個引用計數需要消耗資源,可還是那句話,對于如今的硬件資源來說,是完全可以接受的,畢竟引用計數真的很方便。但是,是的我要說但是了,就是我們之前的那個循環引用的問題。
l1 = []
l2 = []
l1.append(l2)
l2.append(l1)
del l1, l2
初始的時候,l1和l2指向的內存的引用計數都為1,但是l1.append(l2),那么l2指向內存的引用計數變成了2,同理l2.append(l1)導致l1指向內存的引用計數也變成了2。因此當我們del l1,?l2的時候,引用計數會從2變成1,因此l1和l2都不會被回收,因為我們是希望回收l1和l2的,但是如果只有引用計數的話,那么顯然這兩者是回收不了的。因此這算是引用計數的最大的缺陷,因為會導致內存泄漏。因此python為了解決這個問題,就必須在引用計數機制之上又引入了新的主流垃圾回收計數:標記--清除和分代收集計數來彌補這個最致命的漏洞。
17.3.2 三色標記模型
無論何種垃圾回收機制,一般都分為兩個階段:垃圾檢測和垃圾回收。垃圾檢測是從所有的已經分配的內存中區別出"可回收"和"不可回收"的內存,而垃圾回收則是使操作系統重新掌握垃圾檢測階段所標識出來的"可回收"內存塊。所以垃圾回收,并不是說直接把這塊內存的數據清空了,而是說將使用權從新交給了操作系統,不會自己霸占了。下面我們來看看標記--清除(mark--sweep)方法是如何實現的,并為這個過程建立一個三色標記模型,python中的垃圾回收正是基于這個模型完成的。
從具體的實現上來講,標記--清除方法同樣遵循垃圾回收的兩個階段,其簡要過程如下:
尋找根對象(root object)的集合,所謂的root object就是一些全局引用和函數棧的引用。這些引用所用的對象是不可被刪除的,而這個root object集合也是垃圾檢測動作的起點
從root object集合出發,沿著root object集合中的每一個引用,如果能到達某個對象A,則稱A是可達的(reachable),可達的對象也不可被刪除。這個階段就是垃圾檢測階段
當垃圾檢測階段結束后,所有的對象分為了可達的(reachable)和不可達的(unreachable)。而所有可達對象都必須予以保留,而不可達對象所占用的內存將被回收。
在垃圾回收動作被激活之前,系統中所分配的所有對象和對象之間的引用組成了一張有向圖,其中對象是圖中的節點,而對象間的引用則是圖的邊。我們在這個有向圖的基礎之上建立一個三個標注模型,更形象的展示垃圾回收的整個動作。當垃圾回收開始時,我們假設系統中的所有對象都是不可達的,對應在有向圖上就是白色 。隨后從垃圾回收的動作開始,沿著始于root object集合中的某個object的引用鏈,在某個時刻到達了對象A,那我們把A標記為灰色,灰色表示一個對象是可達的,但是其包含的引用還沒有被檢查。當我們檢查了對象A所包含的所有引用之后,A將被標記為黑色,表示其包含的所有引用已經被檢查過了。顯然,此時A中引用的對象則被標記成了灰色。假如我們從root object集合開始,按照廣度優先的策略進行搜索的話,那么不難想象,灰色節點對象集合就如同波紋一樣,不斷向外擴散,隨著所有的灰色節點都變成了黑色節點,也就意味著垃圾檢測階段結束了。
17.4 python中的垃圾回收
如之前所說,python中主要的內存管理手段是引用計數機制,而標記--清除和分代收集只是為了打破循環引用而引入的補充技術。這一事實意味著python中的垃圾回收只關注可能會產生循環引用的對象,而像PyLongObject、PyUnicodeObject這些對象是絕對不可能產生循環引用的,因為它們內部不可能持有對其他對象的引用,所以這些直接通過引用計數機制就可以實現,而且后面我們說的垃圾回收也專指那些可能產生循環引用的對象。python中的循環引用只會總是發生在container對象之間,所謂container對象就是內部可持有對其他對象的引用的對象,比如list、dict、class、instance等等。當python開始垃圾回收機制開始運行時,只需要檢查這些container對象,而對于PyLongObject、PyUnicodeObject則不需要理會,這使得垃圾回收帶來的開銷只依賴于container對象的數量,而非所有對象的數量。為了達到這一點,python就必須跟蹤所創建的每一個container對象,并將這些對象組織到一個集合中,只有這樣,才能將垃圾回收的動作限制在這些對象上。而python采用了一個雙向鏈表,所有的container對象在創建之后,都會被插入到這個鏈表當中。
17.4.1 可收集對象鏈表
在對python對象機制的分析當中我們已經看到,任何一個python對象都可以分為兩部分,一部分是PyObject_HEAD,另一部分是對象自身的數據。然而對于一個需要被垃圾回收機制跟蹤的container來說,還不夠,因為這個對象還必須鏈入到python內部的可收集對象鏈表中。而一個container對象要想成為一個可收集的對象,則必須加入額外的信息,這個信息位于PyObject_HEAD之前,稱為PyGC_Head
//objimpl.h
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy; /* force worst-case alignment */
// malloc returns memory block aligned for any built-in types and
// long double is the largest standard C type.
// On amd64 linux, long double requires 16 byte alignment.
// See bpo-27987 for more discussion.
} PyGC_Head;
所以,對于python所創建的可收集container對象,其內存分布與我們之前所了解的內存布局是不同的,我們可以從可收集container對象的創建過程中窺見其內存分布。
//Modules/gcmodule.c
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
if (op != NULL)
op = PyObject_INIT(op, tp);
return op;
}
PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
return _PyObject_GC_Alloc(0, basicsize);
}
#define GC_UNTRACKED _PyGC_REFS_UNTRACKED
#define _PyGC_REFS_UNTRACKED (-2) //該行位于objimpl.h中
static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
PyObject *op;
PyGC_Head *g;
size_t size;
//將對象和PyGC_Head所需內存加起來
if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
return PyErr_NoMemory();
size = sizeof(PyGC_Head) + basicsize;
//為對象本身和PyGC_Head申請內存
if (use_calloc)
g = (PyGC_Head *)PyObject_Calloc(1, size);
else
g = (PyGC_Head *)PyObject_Malloc(size);
if (g == NULL)
return PyErr_NoMemory();
g->gc.gc_refs = 0;
_PyGCHead_SET_REFS(g, GC_UNTRACKED);
_PyRuntime.gc.generations[0].count++; /* number of allocated GC objects */
if (_PyRuntime.gc.generations[0].count > _PyRuntime.gc.generations[0].threshold &&
_PyRuntime.gc.enabled &&
_PyRuntime.gc.generations[0].threshold &&
!_PyRuntime.gc.collecting &&
!PyErr_Occurred()) {
_PyRuntime.gc.collecting = 1;
collect_generations();
_PyRuntime.gc.collecting = 0;
}
op = FROM_GC(g);
return op;
}
因此我們可以很清晰的看到,當python為可收集的container對象申請內存空間時,為PyGC_Head也申請了空間,并且其位置位于container對象之前。所以對于PyListObject、PyDictObject等container對象的內存分布的推測就應該變成這樣。
在可收集container對象的內存分布中,內存分為三個部分,首先第一塊用于垃圾回收機制,然后緊跟著的是python中所有對象都會有的PyObject_HEAD,最后才是container自身的數據。這里的container對象,既可以是PyDictObject、也可以是PyListObject等等。
//objimpl.h
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy; /* force worst-case alignment */
// malloc returns memory block aligned for any built-in types and
// long double is the largest standard C type.
// On amd64 linux, long double requires 16 byte alignment.
// See bpo-27987 for more discussion.
} PyGC_Head;
再來看看PyGC_Head的模樣,里面除了兩個建立鏈表結構的前向和后向指針外,還有一個gc_ref,而這個值被初始化為GC_UNTRACKED,在上面的代碼中可以看到。這個變量對于垃圾回收的運行至關重要,但是在分析它之前我們還需要了解一些其他的東西。
當垃圾回收機制運行期間,我們需要在一個可收集的container對象的PyGC_Head部分和PyObject_HEAD部分之間來回切換。更清楚的說,某些時候,我們持有一個對象A的PyObject_HEAD的地址,但是我們需要根據這個地址來獲得PyGC_Head的地址;而且某些時候,我們又需要反過來進行逆運算。而python提供了兩個地址之間的轉換算法
//gcmodule.c
//AS_GC,根據PyObject_HEAD得到PyGC_Head
#define AS_GC(o) ((PyGC_Head *)(o)-1)
//FROM_GC,從PyGC_Head那里得到PyObject_HEAD
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
//objimpl.h
#define _Py_AS_GC(o) ((PyGC_Head *)(o)-1)
在PyGC_Head中,出現了用于建立鏈表的兩個指針,只有將創建的可收集container對象鏈接到python內部維護的可收集對象鏈表中,python的垃圾回收機制才能跟蹤和處理這個container對象。但是我們發現,在創建可收集container對象之時,并沒有立刻將這個對象鏈入到鏈表中。實際上,這個動作是發生在創建某個container對象最后一步,以PyListObject的創建舉例。
//listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
PyListObject *op;
...
Py_SIZE(op) = size;
op->allocated = size;
//創建PyListObject對象、并設置完屬性之后,返回之前,通過這一步_PyObject_GC_TRACK將所創建的container對象鏈接到了python中的可收集對象鏈表中。
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
//objimpl.h
#define _PyObject_GC_TRACK(o) do { \
PyGC_Head *g = _Py_AS_GC(o); \
if (_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED) \
Py_FatalError("GC object already tracked"); \
_PyGCHead_SET_REFS(g, _PyGC_REFS_REACHABLE); \
g->gc.gc_next = _PyGC_generation0; \
g->gc.gc_prev = _PyGC_generation0->gc.gc_prev; \
g->gc.gc_prev->gc.gc_next = g; \
_PyGC_generation0->gc.gc_prev = g; \
} while (0);
前面我們說過,python會將自己的垃圾回收機制限制在其維護的可收集對象鏈表上,因為所有的循環引用一定是發生這個鏈表的一群對象之間。在_PyObject_GC_TRACK之后,我們創建的container對象也就置身于python垃圾回收機制的掌控機制當中了。
同樣的,python還提供將一個container對象從鏈表中摘除的方法,顯然這個方法應該會在對象被銷毀的時候調用。
//objimpl.h
#define _PyObject_GC_UNTRACK(o) do { \
PyGC_Head *g = _Py_AS_GC(o); \
assert(_PyGCHead_REFS(g) != _PyGC_REFS_UNTRACKED); \
_PyGCHead_SET_REFS(g, _PyGC_REFS_UNTRACKED); \
g->gc.gc_prev->gc.gc_next = g->gc.gc_next; \
g->gc.gc_next->gc.gc_prev = g->gc.gc_prev; \
g->gc.gc_next = NULL; \
} while (0);
很明顯,_PyObject_GC_UNTRACK只是_PyObject_GC_TRACK的逆運算而已
17.4.2 分代的垃圾收集
無論什么語言,寫出來的程序都有共同之處。那就是不同對象的聲明周期會存在不同,有的對象所占的內存塊的生命周期很短,而有的內存塊的生命周期則很長,甚至可能從程序的開始持續到程序結束。這兩者的比例大概在80~90%
這對于垃圾回收機制有著重要的意義,因為我們已經知道,像標記--清除這樣的垃圾回收機制所帶來的額外操作實際上是和系統中內存塊的數量是相關的,當需要回收的內存塊越多的時候,垃圾檢測帶來的額外操作就越多,相反則越少。因此我們可以采用一種空間換時間的策略,因為目前所有對象都在一個鏈子上,每當進行垃圾回收機制的時候,都要把所有對象都檢查一遍。而其實也有不少比較穩定的對象(在多次垃圾回收的洗禮下能活下來),我們完全沒有必要每次都檢查,或者說檢查的頻率可以降低一些。于是聰明如你已經猜到了,我們再來一根鏈子不就可以了,把那些認為比較穩定的對象移到另外一條鏈子上,而新的鏈子進行垃圾回收的頻率會低一些,總之頻率不會像初始的鏈子那么高。
所以這種思想就是:將系統中的所有內存塊根據其存活時間劃分為不同的集合,每一個集合就成為一個"代",垃圾回收的頻率隨著"代"的存活時間的增大而減小,也就是說,存活的越長的對象就越可能不是垃圾,就越可能是程序中需要一直存在的對象,就應該少去檢測它。反正不是垃圾,你檢了也白檢。那么關鍵的問題來了,這個存活時間是如何被衡量的呢?或者我們說當對象比較穩定的時候的這個穩定是如何衡量的呢?沒錯,我們上面已經暴露了,就是通過經歷了幾次垃圾回收動作來評判,如果一個對象經歷的垃圾回收次數越多,那么顯然其存活時間就越長。因為python的垃圾回收器,每當條件滿足時(至于什么條件我們后面會說),就會進行一次垃圾回收(注意:不同的代的垃圾回收的頻率是不同的),而每次掃黃的時候你都不在,吭,每次垃圾回收的時候你都能活下來,這就說明你存活的時間更長,或者像我們上面說的更穩定,那么就不應該再把你放在這個鏈子上了,而是會移動到新的鏈子上。而在新的鏈子上,進行垃圾回收的頻率會降低,因為既然穩定了,檢測就不必那么頻繁了,或者說新的鏈子上觸發垃圾回收所需要的時間更長了。
"代"似乎是一個比較抽象的概念,但在python中,你就把"代"想象成多個對象組成集合,或者你把"代"想象成鏈表(或者鏈子)也可以,因為這些對象都串在鏈表上面。而屬于同一"代"的內存塊都被鏈接在同一個鏈表中。而在python中總共存在三條鏈表,說明python中所有的對象總共可以分為三代,分別零代、一代、二代。一個"代"就是一條我們上面提到的可收集對象鏈表。而在前面所介紹的鏈表的基礎之上,為了支持分代機制,我們需要的僅僅是一個額外的表頭而已。
//Include/internal/mem.h
struct gc_generation {
PyGC_Head head;
int threshold; /* collection threshold */
int count; /* count of allocations or collections of younger
generations */
};
#define NUM_GENERATIONS 3
//gcmodule.c
#define GEN_HEAD(n) (&_PyRuntime.gc.generations[n].head)
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{_GEN_HEAD(0), _GEN_HEAD(0), 0}}, 700, 0},
{{{_GEN_HEAD(1), _GEN_HEAD(1), 0}}, 10, 0},
{{{_GEN_HEAD(2), _GEN_HEAD(2), 0}}, 10, 0},
};
state->generation0 = GEN_HEAD(0);
上面這個維護了三個gc_generation結構的數組,通過這個數組控制了三條可收集對象鏈表,這就是python中用于分代垃圾收集的三個"代"。
而我們在之前上面說的_PyObject_GC_TRACK中會看到_PyGC_generation0,它不偏不斜,指向的正是第0代鏈表。
對于每一個gc_generation,其中的count記錄了當前這條可收集對象鏈表中一共有多少個對象。而在_PyObject_GC_Alloc中我們可以看到每當分配了內存,就會進行_PyRuntime.gc.generations[0].count++動作,將第0代鏈表中所維護的內存塊數量加1,這預示著所有新創建的對象實際上都會被加入到0代鏈表當中,而這一點也確實如此,已經被_PyObject_GC_TRACK證明了。而且我們發現這里是先將數量加1,然后再將新的container對象(內存塊)才會被鏈接到第0代鏈表當中,當然這個無所謂啦。
而gc_generation中的threshold則記錄該條可收集對象鏈表中最多可以容納多少個可收集對象,從python的實現代碼中,我們知道第0代鏈表中最多可以容納700個對象(只可能是container對象)。而一旦第0代鏈表中的container對象超過了700個這個閾值,那么會立刻除法垃圾回收機制。
static Py_ssize_t
collect_generations(void)
{
int i;
Py_ssize_t n = 0;
for (i = NUM_GENERATIONS-1; i >= 0; i--) {
//當count大于threshold的時候,但是這個僅僅針對于0代鏈表
if (_PyRuntime.gc.generations[i].count > _PyRuntime.gc.generations[i].threshold) {
if (i == NUM_GENERATIONS - 1
&& _PyRuntime.gc.long_lived_pending < _PyRuntime.gc.long_lived_total / 4)
continue;
n = collect_with_callback(i);
break;
}
}
return n;
}
這里面雖然寫了一個for循環,但是只有當第0代鏈表的count超過了threshold的時候才會觸發垃圾回收,那么1代鏈表和2代鏈表觸發垃圾回收的條件又是什么呢?當0代鏈表觸發了10次垃圾回收的時候,會觸發一次1代鏈表的垃圾回收。當1代鏈表觸發了10次垃圾回收的時候,會觸發一次2代鏈表的垃圾回收。另外:
在清理1代鏈表的時候,會順帶清理0代鏈表
在清理2代鏈表的時候,會順帶清理0代鏈表和1代鏈表
17.4.3 python中的標記--清除
我們上面說到,當清理1代鏈表會順帶清理0代鏈表,總是就是把比自己"代"要小的鏈子也清理了。那么這是怎么做到的呢?其實答案就在gc_list_merge函數中,如果清理的是1代鏈表,那么在開始垃圾回收之前,python會將0代鏈表(比它年輕的),整個地鏈接到1代鏈表之后。
//gcmodule.c
static void
gc_list_merge(PyGC_Head *from, PyGC_Head *to)
{
PyGC_Head *tail;
assert(from != to);
if (!gc_list_is_empty(from)) {
tail = to->gc.gc_prev;
tail->gc.gc_next = from->gc.gc_next;
tail->gc.gc_next->gc.gc_prev = tail;
to->gc.gc_prev = from->gc.gc_prev;
to->gc.gc_prev->gc.gc_next = to;
}
gc_list_init(from);
}
以我們舉的例子來說的話,那么這里的from就是0代鏈表,to就是1代鏈表,所以此后的標記--清除算法就將在merge之后的那一條鏈表上進行。
在介紹python中的標記--清除垃圾回收方法之前,我們需要建立一個循環引用的最簡單例子
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
# 注意這里多了一個外部引用
a = list1
list3 = []
list4 = []
list3.append(list4)
list4.append(list3)
上面的數字指的是當前對象的引用計數ob_refcnt的值
17.4.3.1 尋找root object集合
為了使用標記--清除算法,按照我們之前對垃圾收集算法的一般性描述,首先我們需要找到root object,那么在我們上面的那幅圖中,哪些是屬于root object呢?
讓我們換個角度來思考,前面提到,root object是不能被刪除的對象。也就是說,在可收集對象鏈表的外部存在著某個引用在引用這個對象,刪除這個對象會導致錯誤的行為,那么在我們當前這個例子中只有list1是屬于root object的。但這僅僅是觀察的結果,那么如何設計一種算法來得到這個結果呢?
我們注意到這樣一個事實,如果兩個對象的引用計數都為1,但是僅僅它們之間存在著循環引用,那么這兩個對象是需要被回收的,也就是說,盡管它們的引用計數表現為非0,但是實際上有效的引用計數為0。這里,我們提出了有效引用計數的概念,為了從引用計數中獲得優秀的引用計數,必須將循環引用的影響取出,也就是說,這個閉環從引用中摘除,而具體的實現就是兩個對象各自的引用值都減去1。這樣一來,兩個對象的引用計數都成為了0,這樣我們便揮去了循環引用的迷霧,是有效引用計數出現了真身。那么如何使兩個對象的引用計數都減1呢,很簡單,假設這兩個對象為A和B,那么從A出發,由于它有一個對B的引用,則將B的引用計數減1;然后順著引用達到B,發現它有一個對A的引用,那么同樣會將A的引用減1,這樣就完成了循環引用對象間環的刪除。
總結一下就是,python會尋找那些具有循環引用的、但是沒有被外部引用的對象,并嘗試把它們的引用計數都減去1
但是這樣就引出了一個問題,假設可收集對象鏈表中的container對象A有一個對對象C的引用,而C并不在這個鏈表中,如果將C的引用計數減去1,而最后A并沒有被回收,那么顯然,C的引用計數被錯誤地減少1,這將導致未來的某個時刻對C的引用會出現懸空。這就要求我們必須在A沒有被刪除的情況下回復C的引用計數,可是如果采用這樣的方案的話,那么維護引用計數的復雜度將成倍增長。換一個角度,其實我們有更好的做法,我們不改動真實的引用計數,而是改動引用計數的副本。對于副本,我們無論做什么樣的改動,都不會影響對象生命周期的維護,因為這個副本的唯一作用就是尋找root? object集合,而這個副本就是PyGC_Head中的gc.gc_ref。在垃圾回收的第一步,就是遍歷可收集對象鏈表,將每個對象的gc.gc_ref的值設置為其ob_refcnt的值。
//gcmodule.c
static void
update_refs(PyGC_Head *containers)
{
PyGC_Head *gc = containers->gc.gc_next;
for (; gc != containers; gc = gc->gc.gc_next) {
assert(_PyGCHead_REFS(gc) == GC_REACHABLE);
_PyGCHead_SET_REFS(gc, Py_REFCNT(FROM_GC(gc)));
assert(_PyGCHead_REFS(gc) != 0);
}
}
//而接下來的動作就是要將環引用從引用中摘除
static void
subtract_refs(PyGC_Head *containers)
{
traverseproc traverse;
PyGC_Head *gc = containers->gc.gc_next;
for (; gc != containers; gc=gc->gc.gc_next) {
traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
(void) traverse(FROM_GC(gc),
(visitproc)visit_decref,
NULL);
}
}
我們注意到里面有一個traverse,這個是和特定的container 對象有關的,在container對象的類型對象中定義。一般來說,traverse的動作就是遍歷container對象中的每一個引用,然后對引用進行某種動作,而這個動作在subtract_refs中就是visit_decref,它以一個回調函數的形式傳遞到traverse操作中。比如:我們來看看PyListObject對象所定義traverse操作。
//object.h
typedef int (*visitproc)(PyObject *, void *);
typedef int (*traverseproc)(PyObject *, visitproc, void *);
//listobject.c
PyTypeObject PyList_Type = {
...
(traverseproc)list_traverse, /* tp_traverse */
...
};
static int
list_traverse(PyListObject *o, visitproc visit, void *arg)
{
Py_ssize_t i;
for (i = Py_SIZE(o); --i >= 0; )
//對列表中的每一個元素都進行回調的操作
Py_VISIT(o->ob_item[i]);
return 0;
}
//gcmodule.c
/* A traversal callback for subtract_refs. */
static int
visit_decref(PyObject *op, void *data)
{
assert(op != NULL);
//PyObject_IS_GC判斷op指向的對象是不是被垃圾收集監控的
//標識container對象是被垃圾收集監控的
if (PyObject_IS_GC(op)) {
PyGC_Head *gc = AS_GC(op);
assert(_PyGCHead_REFS(gc) != 0); /* else refcount was too small */
if (_PyGCHead_REFS(gc) > 0)
_PyGCHead_DECREF(gc);
}
return 0;
}
在完成了subtract_refs之后,可收集對象鏈表中所有container對象之間的環引用就被摘除了。這時有一些container對象的PyGC_Head.gc_ref還不為0,這就意味著存在對這些對象的外部引用,這些對象就是開始標記--清除算法的root object。
估計有人不明白引用計數是加在什么地方,其實變量=值在python中,變量得到的都是值的指針,a =?1,表示是在命名空間里面會有"a": 1這個鍵值對,但看似是這樣,其實存儲的并不是1,而是1這個結構體(python對象在底層是一個結構體)的指針,這個結構體存儲在堆區。我們獲取a的引用計數,其實是獲取a指向的這個對象的引用計數,此時為1,如果b=a,在底層就等價于把a存儲的內容(指針)拷貝給了b,那么此時a和b存儲的指針指的都是同一個對象,那么這個對象的引用計數就變成了2。如果再來個b=2,那么表示再創建一個結構體存儲的值為2,然后讓b存儲新的結構體的指針。那么原來的結構體的引用計數就從2又變成了1。
所以為什么初始的時候,list1的引用計數是3就很明顯了,list1的引用計數指的其實是list1這個變量對應的值(或者說在底層,list1存儲的指針指向的值)的引用計數,所以一旦創建一個變量那么引用計數會自動增加為1,然后a也指向了list1所指向的內存,并且list1又作為list2的一個元素(這個位置的元素存儲了指向list1的指針),所以引用計數總共是3。
由于sys.getrefcount函數本身會多一個引用,所以減去1的話,那么都是3。表示它們指向的內存存儲的值的引用計數為3。sys.getrefcount(a) -> 4,這個時候a就想到了,除了我,還有兩位老鐵指向了我指向的內存。
17.4.3.2 垃圾標記
假設我們現在執行了刪除操作del?list1, list2,?list3, list4,那么成功地尋找到root object集合之后,我們就可以從root object觸發,沿著引用鏈,一個接一個地標記不能回收的內存,由于root object集合中的對象是不能回收的,因此,被這些對象直接或間接引用的對象也是不能回收的,比如這里的list2,即便del list2,但是因為list1不能回收,而又append了list2,所以list2指向的內存也是不可以釋放的。下面在從root object出發前,我們首先需要將現在的內存鏈表一分為二,一條鏈表維護root object集合,成為root鏈表,而另一條鏈表中維護剩下的對象,成為unreachable鏈表。之所以要分解成兩個鏈表,是出于這樣一種考慮:顯然,現在的unreachable鏈表是名不副實的,因為里面可能存在被root鏈表中的對象直接或者間接引用的對象,這些對象也是不可以回收的,因此一旦在標記中發現了這樣的對象,那么就應該將其從unreachable中移到root鏈表中;當完成標記之后,unreachable鏈表中剩下的對象就是名副其實的垃圾對象了,那么接下來的垃圾回收只需要限制在unreachable鏈表中即可。
為此python專門準備了一條名為unreachable的鏈表,通過move_unreachable函數完成了對原始鏈表的切分。
//gcmodule.c
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
PyGC_Head *gc = young->gc.gc_next;
while (gc != young) {
PyGC_Head *next;
//[1]:如果是root object
if (_PyGCHead_REFS(gc)) {
PyObject *op = FROM_GC(gc);
traverseproc traverse = Py_TYPE(op)->tp_traverse;
assert(_PyGCHead_REFS(gc) > 0);
//設置其gc_refs為GC_REACHABLE
_PyGCHead_SET_REFS(gc, GC_REACHABLE);
(void) traverse(op,
(visitproc)visit_reachable,
(void *)young);
next = gc->gc.gc_next;
if (PyTuple_CheckExact(op)) {
_PyTuple_MaybeUntrack(op);
}
}
else {
//[2]:對于非root object,移到unreachable鏈表中
next = gc->gc.gc_next;
gc_list_move(gc, unreachable);
_PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE);
}
gc = next;
}
}
static int
visit_reachable(PyObject *op, PyGC_Head *reachable)
{
if (PyObject_IS_GC(op)) {
PyGC_Head *gc = AS_GC(op);
const Py_ssize_t gc_refs = _PyGCHead_REFS(gc);
//[3]:對于還沒有處理的對象,恢復其gc_refs
if (gc_refs == 0) {
_PyGCHead_SET_REFS(gc, 1);
}
//[4]:對于已經被挪到unreachable鏈表中的對象,將其再次挪動到原來的鏈表
else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
gc_list_move(gc, reachable);
_PyGCHead_SET_REFS(gc, 1);
}
else {
assert(gc_refs > 0
|| gc_refs == GC_REACHABLE
|| gc_refs == GC_UNTRACKED);
}
}
return 0;
}
在move_unreachable中,沿著可收集對象鏈表依次向前,并檢查其PyGC_Head.gc.gc_ref值,我們發現這里的動作是遍歷鏈表,而并非從root object集合出發,遍歷引用鏈。這會導致一個微妙的結果,即當檢查到一個gc_ref為0的對象時,我們并不能立即斷定這個對象就是垃圾對象。因為在這個對象之后的對象鏈表上,也許還會遇到一個root object,而這個root object引用該對象。所以這個對象只是一個可能的垃圾對象,因此我們才要將其標志為GC_TENTATIVELY_UNREACHABLE,但是還是通過gc_list_move將其搬到了unreachable鏈表中,咦,難道不會出問題嗎?別急,我們馬上就會看到, python還留了后手。
當在move_unreachable中遇到一個gc_refs不為0的對象A時,顯然,A是root object或者是從某個root object開始可以引用到的對象,而A所引用的所有對象也都是不可回收的對象。因此在代碼的[1]處下面,我們看到會再次調用與特定對象相關的transverse操作,依次對A所引用的對象調用visit_reachable。在visit_reachable的[4]處我們發現,如果A所引用的對象之前曾被標注為GC_TENTATIVELY_UNREACHABLE,那么現在A可以訪問到它,意味著它也是一個不可回收的對象,所以python會再次從unreachable鏈表中將其搬回到原來的鏈表。注意:這里的reachable,就是move_unreachable中的young,也就是我們所謂的root object鏈表。python還會將其gc_refs設置為1,表示該對象是一個不可回收對象。同樣在[1]處,我們看到對A所引用的gc_refs為0的對象,其gc_refs也被設置成了1。想一想這是什么對象呢?顯然它就是在鏈表move_unreachable操作中還沒有訪問到的對象,這樣python就直接掐斷了之后move_unreachable訪問它時將其移動到unreachable鏈表的誘因。
當move_unreachable完成之后,最初的一條鏈表就被切分成了兩條鏈表,在unreachable鏈表中,就是我們發現的垃圾對象,是垃圾回收的目標。但是等一等,在unreachable鏈表中,所有的對象都可以安全回收嗎?其實,垃圾回收在清理對象的時候,默認是會清理的,但是一旦當我們定義了函數__del__,那么在清理對象的時候就會調用這個__del__方法,因此也叫析構函數,這是python為開發人員提供的在對象被銷毀時進行某些資源釋放的Hook機制。在python3中,即使我們重寫了也沒事,因為python會把含有__del__函數的PyInstanceObject對象都統統移動到一個名為garbage的PyListObject對象中。
17.4.4.4 垃圾回收
要回收unreachable鏈表中的垃圾對象,就必須先打破對象間的循環引用,前面我們已經闡述了如何打破循環引用的辦法,下面來看看具體的銷毀過程
//gcmodule.c
static int
gc_list_is_empty(PyGC_Head *list)
{
return (list->gc.gc_next == list);
}
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)
{
inquiry clear;
while (!gc_list_is_empty(collectable)) {
PyGC_Head *gc = collectable->gc.gc_next;
PyObject *op = FROM_GC(gc);
if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {
PyList_Append(_PyRuntime.gc.garbage, op);
}
else {
if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
Py_INCREF(op);
clear(op);
Py_DECREF(op);
}
}
if (collectable->gc.gc_next == gc) {
/* object is still alive, move it, it may die later */
gc_list_move(gc, old);
_PyGCHead_SET_REFS(gc, GC_REACHABLE);
}
}
}
其中會調用container對象的類型對象中的tp_clear操作,這個操作會調整container對象中引用的對象的引用計數值,從而打破完成循環的最終目標。還是以PyListObject為例:
//listobject.c
static int
_list_clear(PyListObject *a)
{
Py_ssize_t i;
PyObject **item = a->ob_item;
if (item != NULL) {
i = Py_SIZE(a);
//將ob_size調整為0
Py_SIZE(a) = 0;
//ob_item是一個二級指針,本來指向一個數組的指針
//現在指向為NULL
a->ob_item = NULL;
//容量也設置為0
a->allocated = 0;
while (--i >= 0) {
//數組里面元素也全部減少引用計數
Py_XDECREF(item[i]);
}
//釋放數組
PyMem_FREE(item);
}
return 0;
}
我們注意到,在delete_garbage中,有一些unreachable鏈表中的對象會被重新送回到reachable鏈表(即delete_garbage的old參數)中,這是由于進行clear動作時,如果成功進行,則通常一個對象會把自己從垃圾回收機制維護的鏈表中摘除(也就是這里的collectable鏈表)。由于某些原因,對象可能在clear動作時,沒有成功完成必要的動作,從而沒有將自己從collectable鏈表摘除,這表示對象認為自己還不能被銷毀,所以python需要講這種對象放回到reachable鏈表中。
我們在上面看到了list_clear,假設是調用了list3的list_clear,那么不好意思,這個是對list4做的處理。因為list3和list4存在循環引用,如果調用了list3的list_clear會減少list4的引用計數,由于這兩位老鐵都被刪除了,還惺惺相惜賴在內存里面不走,所以將list4的引用計數減少1之后,只能歸于湮滅了,然后會調用其list_dealloc,注意:這時候調用的是list4的list_dealloc。
//listobjct.c
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
//從可收集鏈表中移除
PyObject_GC_UnTrack(op);
Py_TRASHCAN_SAFE_BEGIN(op)
if (op->ob_item != NULL) {
//依次遍歷,減少內部元素的引用計數
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]);
}
//釋放內存
PyMem_FREE(op->ob_item);
}
//緩沖池機制
if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
free_list[numfree++] = op;
else
Py_TYPE(op)->tp_free((PyObject *)op);
Py_TRASHCAN_SAFE_END(op)
}
我們知道調用list3的list_clear,減少內部元素引用計數的時候,導致list4引用計數為0。而一旦list4的引用計數為0,那么是不是也要執行和list3一樣的list_clear動作呢?然后會發現list3的引用計數也為0了,因此list3也會被銷毀。循環引用,彼此共生,銷毀之路,怎能獨自前行?最終list3和list4都會執行內部的list_dealloc,釋放內部元素,調整參數,當然還有所謂的緩沖池機制等等。總之如此一來,list3和list4就都被安全地回收了。
17.4.4.5 總結
雖然有很多對象掛在垃圾收集機制監控的鏈表上,但是很多時候是引用計數機制在維護這些對象,只有引用計數無能為力的循環引用,垃圾收集機制才會起到作用(這里沒有把引用計數機制看成垃圾回收,當然如果別人問你python的垃圾回收機制的時候,你也可以把引用計數機制加上)。事實上,如果不是循環引用的話,那么垃圾回收是無能為力的,因為掛在垃圾回收機制上的對象都是引用計數不為0的,如果為0早被引用計數機制干掉了。而引用計數不為0的情況只有兩種:一種是被程序使用的對象,二是循環引用中的對象。被程序使用的對象是不能被回收的,所以垃圾回收只能處理那些循環引用的對象。
所以python的垃圾回收就是:引用計數為主,分代回收為輔,兩者結合使用,后者主要是為了彌補前者的缺點而存在的。
17.5 python中的gc模塊
這個gc模塊,底層就是gcmodule,我們說這些模塊底層是用c寫的,當python編譯好時,就內嵌在解釋器里面了。我們可以導入它,但是在python安裝目錄上看不到。
gc.enable():開啟垃圾回收
這個函數表示開啟垃圾回收機制,默認是自動開啟的。
gc.disable():關閉垃圾回收
import gc
class A:
pass
# 關掉gc
gc.disable()
while True:
a1 = A()
a2 = A()
# 此時內部出現了循環引用
a1.__dict__["attr"] = a2
a2.__dict__["attr"] = a1
# 由于循環引用,此時是del a1, a2,光靠引用計數是刪不掉的
# 需要垃圾回收,但是我們給關閉了
del a1, a2
無限循環,并且每次循環都會創建新的對象,最終導致內存無限增大。
import gc
class A:
pass
# 關掉gc
gc.disable()
while True:
a1 = A()
a2 = A()
這里即使我們關閉了gc,但是每一次循環都會指向一個新的對象,而之前的對象由于沒有人指向了,那么引用計數為0,直接就被引用計數機制干掉了,內存會一直穩定,不會出現增長。所以我們看到,即使關閉了gc,但是對于那些引用計數為0的,該刪除還是會刪除的。所以引用計數很簡單,就是按照對應的規則該加1加1,該減1減1,一旦為0直接銷毀。而當出現循環引用的時候,才需要gc閃亮登場。這里關閉了gc,但是沒有循環引用所以沒事,而上一個例子,關閉了gc,但是出現了循環引用,而引用計數機制只會根據引用計數來判斷,而發現引用計數不為0,所以就一直傻傻地不回收,程序又一直創建新的對象,最終導致內存越用越多。而上一個例子若是開啟了gc,那么分代回收計數,就會通過標記--清除的方式將產生循環引用的對象的引用計數減1,而引用計數機制發現引用計數為0了,那么就會將對象回收掉。所以這個引用計數機制到底算不算垃圾回收機制的一種呢?你要說算吧,我把gc關閉了,引用計數機制還可以發揮作用,你要說不算吧,它確實是負責判定對象是否應該被回收的唯一標準,所以該怎么說就具體看情況吧。
gc.isenabled():判斷gc是否開啟
import gc
print(gc.isenabled()) # True
gc.disable()
print(gc.isenabled()) # False
gc.collect():立刻觸發垃圾回收
我們說,垃圾回收觸發是需要條件的,比如0代鏈表,清理零代鏈表的時候,需要對象的個數count大于閾值threshold(默認是700),但是這個函數可以強制觸發垃圾回收。
gc.get_threshold():返回每一代的閾值
import gc
print(gc.get_threshold()) # (700, 10, 10)
# 700:零代鏈表的對象超過700個,觸發垃圾回收
# 10:零代鏈表,垃圾回收10次,會清理一代鏈表
# 10:一代鏈表,垃圾回收10次,會清理二代鏈表
gc.set_threshold():設置每一代的閾值
import gc
gc.set_threshold(1000, 100, 100)
print(gc.get_threshold()) # (1000, 100, 100)
gc.get_count():查看每一代的值達到了多少
import gc
print(gc.get_count()) # (44, 7, 5)
gc.get_stats():返回每一代的具體信息
from pprint import pprint
import gc
pprint(gc.get_stats())
"""
[{'collected': 316, 'collections': 62, 'uncollectable': 0},
{'collected': 538, 'collections': 5, 'uncollectable': 0},
{'collected': 0, 'collections': 0, 'uncollectable': 0}]
"""
gc.get_objects():返回被垃圾回收器追蹤的所有對象,一個列表
gc.is_tracked(obj):查看對象obj是否被垃圾收集器追蹤
import gc
a = 1
b = []
print(gc.is_tracked(a)) # False
print(gc.is_tracked(b)) # True
# 我們說只有那些可能會產生循環引用的對象才會被垃圾回收器跟蹤
gc.get_referrers(obj):返回所有引用了obj的對象
gc.get_referents(obj):返回所有被obj引用了的對象
gc.freeze():凍結所有被垃圾回收器跟蹤的對象并在以后的垃圾回收中不被處理
gc.unfreeze():取消所有凍結的對象,讓它們繼續參數垃圾回收
gc.get_freeze_count():獲取凍結的對象個數
import gc
# 不需要參數,會自動找到被垃圾回收器跟蹤的對象
gc.freeze()
# 說明有很多內置對象在被跟蹤,被我們凍結了
print(gc.get_freeze_count()) # 24397
b = []
gc.freeze()
# 只要這里比上面多1個就行
print(gc.get_freeze_count()) # 24398
# 取消凍結
gc.unfreeze()
print(gc.get_freeze_count()) # 0
gc.get_debug():獲取debug級別
import gc
print(gc.get_debug()) # 0
gc.set_debug():設置debug級別
import gc
"""
DEBUG_STATS - 在垃圾收集過程中打印所有統計信息
DEBUG_COLLECTABLE - 打印發現的可收集對象
DEBUG_UNCOLLECTABLE - 打印unreachable對象(除了uncollectable對象)
DEBUG_SAVEALL - 將對象保存到gc.garbage(一個列表)里面,而不是釋放它
DEBUG_LEAK - 對內存泄漏的程序進行debug (everything but STATS).
"""
class A:
pass
class B:
pass
a = A()
b = B()
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_SAVEALL)
print(gc.garbage) # []
a.b = b
b.a = a
del a, b
gc.collect() # 強制觸發垃圾回收
# 下面都是自動打印的
"""
gc: collecting generation 2...
gc: objects in each generation: 123 3732 20563
gc: objects in permanent generation: 0
gc: done, 4 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 24249
gc: objects in permanent generation: 0
gc: done, 0 unreachable, 0 uncollectable, 0.0150s elapsed
gc: collecting generation 2...
gc: objects in each generation: 525 0 23752
gc: objects in permanent generation: 0
gc: done, 7062 unreachable, 0 uncollectable, 0.0000s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 21941
gc: objects in permanent generation: 0
gc: done, 4572 unreachable, 0 uncollectable, 0.0000s elapsed
"""
print(gc.garbage)
# [<__main__.a object at>, <__main__.b object at>, {'b': <__main__.b object at>}, {'a': <__main__.a object at>}]
17.6 總結
盡管python采用了最經典的(最土的)的引用計數來作為自動內存管理的方案,但是python采用了多種方式來彌補引用計數的不足,內存池的大量使用,標記--清除(分代技術采用的去除循環引用的引用計數的方式)垃圾收集技術都極大地完善了python的內存管理(包括申請、回收)機制。盡管引用計數機制需要花費額外的開銷來維護引用計數,但是現在這個年代,這點內存算個啥。而且引用計數也有好處,不然早就隨著時代的前進而被掃進歷史的垃圾堆里面了。首先引用計數真的很方便,很直觀,對于很多對象引用計數能夠直接解決,不需要什么復雜的操作;另外引用計數將垃圾回收的開銷分攤在了整個運行時,這對于python的響應是有好處的。
當然內存管理和垃圾回收是一門給常精細和繁瑣的技術,有興趣的話各位可以自己大刀闊斧的沖進python的源碼中自由翱翔。
總結
以上是生活随笔為你收集整理的python内存管理和释放_《python解释器源码剖析》第17章--python的内存管理与垃圾回收...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跨计算机建立视图_计算机二级office
- 下一篇: 崩坏三x86架构闪退_不给X86留活路?