解密列表的創(chuàng)建與銷毀,以及緩存池長什么樣子?
楔子
前面我們分析了列表的底層結(jié)構(gòu)和擴容機制,本篇文章來聊一聊列表的創(chuàng)建和銷毀,以及緩存池。
列表的創(chuàng)建
創(chuàng)建列表,解釋器只提供了唯一的一個 Python/C API,也就是 PyList_New。這個函數(shù)接收一個 size 參數(shù),允許我們在創(chuàng)建 PyListObject 對象時指定底層的 PyObject * 數(shù)組的長度。
//Objects/listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
// 聲明一個 PyListObject * 變量
// 指向即將創(chuàng)建的 PyListObject 對象
PyListObject *op;
// 底層數(shù)組的長度必須大于等于 0
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
// PyList_MAXFREELIST 是一個宏,表示緩存池的容量
// 編譯時可以選擇是否禁用緩存池,默認不禁用,容量為 80
#if PyList_MAXFREELIST > 0
// 獲取進程狀態(tài)對象內(nèi)部的緩存池
struct _Py_list_state *state = get_list_state();
// state->numfree 表示緩存池中已緩存的元素個數(shù)
// 如果大于 0,證明有可用元素,那么會從緩存池中獲取
if (PyList_MAXFREELIST && state->numfree) {
// 可用元素的數(shù)量減一
state->numfree--;
// 獲取緩存的列表指針,并將指向的列表的引用計數(shù)設(shè)置為 1
op = state->free_list[state->numfree];
OBJECT_STAT_INC(from_freelist);
_Py_NewReference((PyObject *)op);
}
else
#endif
{
// 如果緩存池被禁用,或者緩存池中沒有可用元素
// 那么通過 PyObject_GC_New 申請內(nèi)存
// 問題來了,之前申請內(nèi)存不是用的 PyObject_New 嗎
// 這里為啥換成 PyObject_GC_New 呢?我們稍后再說
op = PyObject_GC_New(PyListObject, &PyList_Type);
if (op == NULL) {
return NULL;
}
}
// 如果 size <= 0,那么一定等于 0,此時列表不包含任何元素
if (size <= 0) {
// 那么 ob_item 直接設(shè)置為 NULL
op->ob_item = NULL;
}
else {
// 否則為底層數(shù)組申請內(nèi)存,因為存儲的都是指針
// 所以大小為 size * sizeof(PyObject *)
op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}
}
// 將 ob_size 和 allocated 均設(shè)置為 size
Py_SET_SIZE(op, size);
op->allocated = size;
// 讓列表被 GC 跟蹤
_PyObject_GC_TRACK(op);
// 轉(zhuǎn)成泛型指針之后返回
return (PyObject *) op;
}
整個過程非常好理解,就是先創(chuàng)建一個 PyListObject 對象,然后再為底層數(shù)組申請內(nèi)存,最后通過 ob_item 字段將兩者關(guān)聯(lián)起來。當(dāng)然這個過程中會使用緩存池,關(guān)于緩存池一會兒再聊。
然后還要說一下內(nèi)存申請函數(shù),在這之前我們看到申請內(nèi)存用的都是 PyObject_New 函數(shù),它和這里的 PyObject_GC_New 有什么區(qū)別呢?由于涉及到 Python 的內(nèi)存管理,我們暫時先不聊那么深,大家先有個基本了解即可,等到介紹內(nèi)存管理和垃圾回收的時候會詳細剖析。
我們知道 Python 對象在底層都是一個結(jié)構(gòu)體,并且結(jié)構(gòu)體內(nèi)部嵌套了 PyObject。但對于那些能夠產(chǎn)生循環(huán)引用的可變對象來說,它們除了 PyObject 之外,還包含了一個 PyGC_Head,用于垃圾回收。
所以 PyObject_New 和 PyObject_GC_New 接收的參數(shù)是一樣的,但后者會多申請 16 字節(jié)的內(nèi)存,這 16 字節(jié)是為 PyGC_Head 準備的。那么問題來了,PyGC_Head 在什么地方呢?
圖片
PyGC_Head 就在 PyObject 的前面,但是注意:雖然為 PyGC_Head 申請了內(nèi)存,但返回的是 PyObject 的地址。至于這里面的更多細節(jié),后續(xù)在剖析內(nèi)存管理和垃圾回收的時候細說,目前先簡單了解一下即可。
然后再說一下計算內(nèi)存的兩種方式:
import sys
lst = []
# 可以調(diào)用 __sizeof__ 方法計算對象的內(nèi)存
print(lst.__sizeof__()) # 40
# 也可以通過 sys.getsizeof 函數(shù)
print(sys.getsizeof(lst)) # 56
我們看到 sys.getsizeof 算出的結(jié)果會多出 16 字節(jié),相信你能猜到原因,因為它將 PyGC_Head 也算進去了,而對象的 __sizeof__ 方法則不會算在內(nèi)。
不過對于字符串、整數(shù)、浮點數(shù)這種不會產(chǎn)生循環(huán)引用的對象來說,由于沒有 PyGC_Head,所以兩種方式計算的結(jié)果是一樣的。
import sys
print("".__sizeof__()) # 49
print(sys.getsizeof("")) # 49
print((123).__sizeof__()) # 28
print(sys.getsizeof(123)) # 28
以上就是列表的創(chuàng)建,整個過程不難理解。
列表的銷毀
創(chuàng)建 PyListObject 對象時,會先檢測緩存池 free_list 里面是否有可用的對象,有的話直接拿來用,否則通過 malloc 在系統(tǒng)堆上申請。列表的緩存池是使用數(shù)組實現(xiàn)的,里面最多維護 80 個 PyListObject 對象。
// Include/internal/pycore_list.h
#define PyList_MAXFREELIST 80
struct _Py_list_state {
#if PyList_MAXFREELIST > 0
// free_list 是一個 PyListObject * 數(shù)組,容量為 80
// 添加元素時會從數(shù)組的尾部添加
// 獲取元素時也會從數(shù)組的尾部獲取
PyListObject *free_list[PyList_MAXFREELIST];
// 緩存池中可用元素數(shù)量
int numfree;
#endif
};
// Objects/listobject.c
static struct _Py_list_state *
get_list_state(void)
{
// 列表緩存池會在解釋器啟動時創(chuàng)建好,并放在進程狀態(tài)對象中
PyInterpreterState *interp = _PyInterpreterState_GET();
// 返回 struct _Py_list_state 實例
return &interp->list;
}
根據(jù)之前的經(jīng)驗我們知道,既然創(chuàng)建的時候能從緩存池中獲取,那么在執(zhí)行析構(gòu)函數(shù)的時候也要把列表放到緩存池里面。看一下列表的析構(gòu)函數(shù),它由 PyList_Type 的 tp_dealloc 字段負責(zé),而該字段被設(shè)置為 list_dealloc。
// Objects/listobject.c
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
// 列表可能會產(chǎn)生循環(huán)引用,因此創(chuàng)建之后要被 GC 跟蹤
// 而現(xiàn)在要被回收了,所以也要取消 GC 跟蹤
PyObject_GC_UnTrack(op);
// 這一步的作用,稍后再說
Py_TRASHCAN_BEGIN(op, list_dealloc)
// 先釋放底層數(shù)組
if (op->ob_item != NULL) {
// 但是釋放之前,還有一件重要的事情
// 要將底層數(shù)組中每個指針指向的對象的引用計數(shù)都減去 1
// 因為它們不再持有對"對象"的引用
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]);
}
// 然后釋放底層數(shù)組所占的內(nèi)存
PyMem_Free(op->ob_item);
}
#if PyList_MAXFREELIST > 0
// 獲取緩存池
struct _Py_list_state *state = get_list_state();
// 如果已緩存的元素小于 80 個,并且 op 指向的是列表
// 那么將 op 追加到數(shù)組中,并將 numfree 自增 1
if (state->numfree < PyList_MAXFREELIST && PyList_CheckExact(op)) {
state->free_list[state->numfree++] = op;
OBJECT_STAT_INC(to_freelist);
}
else
#endif
{
// 否則將列表的內(nèi)存釋放掉
Py_TYPE(op)->tp_free((PyObject *)op);
}
Py_TRASHCAN_END
}
我們知道創(chuàng)建一個 PyListObject 對象會分為兩步,先創(chuàng)建 PyListObject 對象,然后創(chuàng)建底層數(shù)組,最后讓 PyListObject 對象的 ob_item 字段指向底層數(shù)組的首元素。
同理,在銷毀一個 PyListObject 對象時,會先釋放 ob_item 維護的底層數(shù)組,然后在緩存池已滿的情況下再釋放 PyListObject 對象自身。
現(xiàn)在我們算是明白了緩存池的機制,本來在銷毀列表時,要將它的內(nèi)存釋放。但因為緩存池機制,解釋器并沒有這么做,而是將它的指針放在了緩存池里,至于列表對象則依舊駐留在堆上,只是我們已經(jīng)無法再訪問了。
當(dāng)以后創(chuàng)建新的 PyListObject 對象時,解釋器會首先喚醒這些已經(jīng)死去的 PyListObject 對象,給它們一個洗心革面、重新做人的機會。但需要注意的是,這里緩存的僅僅是 PyListObject 對象,對于底層數(shù)組,其 ob_item 已經(jīng)不再指向了。
從 list_dealloc 中我們可以看到,PyListObject 對象的指針在放進緩存池之前,ob_item 指向的數(shù)組就已經(jīng)被釋放掉了,同時數(shù)組中指針指向的對象的引用計數(shù)會減 1。所以最終數(shù)組中這些指針指向的對象也大難臨頭各自飛了,或生存、或毀滅,總之此時和 PyListObject 之間已經(jīng)沒有任何聯(lián)系了。
但是為什么要這么做呢?為什么不連底層數(shù)組也一起維護呢?可以想一下,如果繼續(xù)維護的話,數(shù)組中指針指向的對象永遠不會被釋放,那么很可能會產(chǎn)生懸空指針的問題。
但實際上是可以將底層數(shù)組進行保留的,做法是只將數(shù)組中指針指向的對象的引用計數(shù)減 1,然后將數(shù)組中的指針都設(shè)置為 NULL,不再指向之前的對象,但并不釋放底層數(shù)組本身所占用的內(nèi)存空間。這樣一來釋放的內(nèi)存不會交給系統(tǒng)堆,那么再次分配的時候,速度會快很多。但這樣會帶來兩個問題。
1)這些內(nèi)存沒人用也會一直占著,并且只能供 PyListObject 對象的 ob_item 指向的底層數(shù)組使用。
2)基于緩存池獲取的列表的容量,和新創(chuàng)建的列表的容量不一定匹配。比如底層數(shù)組長度為 6 的 PyListObject * 被放入了緩存池,那么表示列表最多容納 6 個元素,但如果我們要創(chuàng)建一個長度為 8 的列表怎么辦?此時依舊要重新為底層數(shù)組申請內(nèi)存。
因此基于以上兩個原因,Python 選擇將底層數(shù)組所占的內(nèi)存交還給了系統(tǒng)堆,當(dāng)然也節(jié)省了內(nèi)存。
lst1 = [1, 2, 3]
print(id(lst1)) # 139671899367680
# 扔到緩存池中,放在數(shù)組的尾部
del lst1
# 從緩存池中獲取,也會從數(shù)組的尾部開始拿
lst2 = [1, 2, 3]
print(id(lst2)) # 139671899367680
# 因此打印的地址是一樣的
以上就是列表的創(chuàng)建和銷毀,以及它的緩存池原理。
trashcan 機制
在看列表的銷毀過程時,我們注意到里面有這么一行代碼。
圖片
這是做什么的呢,首先在 Python 中,我們可以創(chuàng)建具有深度遞歸的對象,比如:
L = None
for i in range(2 ** 20):
L = [L]
del L
此時的 L 就是一個嵌套了 2 ** 20 層的列表,當(dāng)我們刪除 L 的時候,會先銷毀 L[0]、然后銷毀 L[0][0],以此類推,直到遞歸深度為 2 ** 20。
而這樣的深度毫無疑問會溢出 C 的調(diào)用棧,導(dǎo)致解釋器崩潰。但事實上我們在 del L 的時候解釋器并沒有崩潰,原因就是 CPython 發(fā)明了一種名為 trashcan 的機制,它通過延遲銷毀的方式來限制銷毀的遞歸深度。關(guān)于這一特性,我們知道就好了,不用太關(guān)注。