楔子
前面我们分析了列表的底层结构和扩容机制,本篇文章来聊一聊列表的创建和销毁,以及缓存池。
列表的创建
创建列表,解释器只提供了唯一的一个 Python/C API,也就是 PyList_New。这个函数接收一个 size 参数,允许我们在创建 PyListObject 对象时指定底层的 PyObject * 数组的长度。
//Objects/listobject.c PyObject * PyList_New(Py_ssize_t size) { // 声明一个 PyListObject * 变量 // 指向即将创建的 PyListObject 对象 PyListObject *op; // 底层数组的长度必须大于等于 0 if (size < 0) { PyErr_BadInternalCall(); return NULL; } // PyList_MAXFREELIST 是一个宏,表示缓存池的容量 // 编译时可以选择是否禁用缓存池,默认不禁用,容量为 80 #if PyList_MAXFREELIST > 0 // 获取进程状态对象内部的缓存池 struct _Py_list_state *state = get_list_state(); // state->numfree 表示缓存池中已缓存的元素个数 // 如果大于 0,证明有可用元素,那么会从缓存池中获取 if (PyList_MAXFREELIST && state->numfree) { // 可用元素的数量减一 state->numfree--; // 获取缓存的列表指针,并将指向的列表的引用计数设置为 1 op = state->free_list[state->numfree]; OBJECT_STAT_INC(from_freelist); _Py_NewReference((PyObject *)op); } else #endif { // 如果缓存池被禁用,或者缓存池中没有可用元素 // 那么通过 PyObject_GC_New 申请内存 // 问题来了,之前申请内存不是用的 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 直接设置为 NULL op->ob_item = NULL; } else { // 否则为底层数组申请内存,因为存储的都是指针 // 所以大小为 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 均设置为 size Py_SET_SIZE(op, size); op->allocated = size; // 让列表被 GC 跟踪 _PyObject_GC_TRACK(op); // 转成泛型指针之后返回 return (PyObject *) op; }
整个过程非常好理解,就是先创建一个 PyListObject 对象,然后再为底层数组申请内存,最后通过 ob_item 字段将两者关联起来。当然这个过程中会使用缓存池,关于缓存池一会儿再聊。
然后还要说一下内存申请函数,在这之前我们看到申请内存用的都是 PyObject_New 函数,它和这里的 PyObject_GC_New 有什么区别呢?由于涉及到 Python 的内存管理,我们暂时先不聊那么深,大家先有个基本了解即可,等到介绍内存管理和垃圾回收的时候会详细剖析。
我们知道 Python 对象在底层都是一个结构体,并且结构体内部嵌套了 PyObject。但对于那些能够产生循环引用的可变对象来说,它们除了 PyObject 之外,还包含了一个 PyGC_Head,用于垃圾回收。
所以 PyObject_New 和 PyObject_GC_New 接收的参数是一样的,但后者会多申请 16 字节的内存,这 16 字节是为 PyGC_Head 准备的。那么问题来了,PyGC_Head 在什么地方呢?
PyGC_Head 就在 PyObject 的前面,但是注意:虽然为 PyGC_Head 申请了内存,但返回的是 PyObject 的地址。至于这里面的更多细节,后续在剖析内存管理和垃圾回收的时候细说,目前先简单了解一下即可。
然后再说一下计算内存的两种方式:
import sys lst = [] # 可以调用 __sizeof__ 方法计算对象的内存 print(lst.__sizeof__()) # 40 # 也可以通过 sys.getsizeof 函数 print(sys.getsizeof(lst)) # 56
我们看到 sys.getsizeof 算出的结果会多出 16 字节,相信你能猜到原因,因为它将 PyGC_Head 也算进去了,而对象的 __sizeof__ 方法则不会算在内。
不过对于字符串、整数、浮点数这种不会产生循环引用的对象来说,由于没有 PyGC_Head,所以两种方式计算的结果是一样的。
import sys print("".__sizeof__()) # 49 print(sys.getsizeof("")) # 49 print((123).__sizeof__()) # 28 print(sys.getsizeof(123)) # 28
以上就是列表的创建,整个过程不难理解。
列表的销毁
创建 PyListObject 对象时,会先检测缓存池 free_list 里面是否有可用的对象,有的话直接拿来用,否则通过 malloc 在系统堆上申请。列表的缓存池是使用数组实现的,里面最多维护 80 个 PyListObject 对象。
// Include/internal/pycore_list.h #define PyList_MAXFREELIST 80 struct _Py_list_state { #if PyList_MAXFREELIST > 0 // free_list 是一个 PyListObject * 数组,容量为 80 // 添加元素时会从数组的尾部添加 // 获取元素时也会从数组的尾部获取 PyListObject *free_list[PyList_MAXFREELIST]; // 缓存池中可用元素数量 int numfree; #endif }; // Objects/listobject.c static struct _Py_list_state * get_list_state(void) { // 列表缓存池会在解释器启动时创建好,并放在进程状态对象中 PyInterpreterState *interp = _PyInterpreterState_GET(); // 返回 struct _Py_list_state 实例 return &interp->list; }
根据之前的经验我们知道,既然创建的时候能从缓存池中获取,那么在执行析构函数的时候也要把列表放到缓存池里面。看一下列表的析构函数,它由 PyList_Type 的 tp_dealloc 字段负责,而该字段被设置为 list_dealloc。
// Objects/listobject.c static void list_dealloc(PyListObject *op) { Py_ssize_t i; // 列表可能会产生循环引用,因此创建之后要被 GC 跟踪 // 而现在要被回收了,所以也要取消 GC 跟踪 PyObject_GC_UnTrack(op); // 这一步的作用,稍后再说 Py_TRASHCAN_BEGIN(op, list_dealloc) // 先释放底层数组 if (op->ob_item != NULL) { // 但是释放之前,还有一件重要的事情 // 要将底层数组中每个指针指向的对象的引用计数都减去 1 // 因为它们不再持有对"对象"的引用 i = Py_SIZE(op); while (--i >= 0) { Py_XDECREF(op->ob_item[i]); } // 然后释放底层数组所占的内存 PyMem_Free(op->ob_item); } #if PyList_MAXFREELIST > 0 // 获取缓存池 struct _Py_list_state *state = get_list_state(); // 如果已缓存的元素小于 80 个,并且 op 指向的是列表 // 那么将 op 追加到数组中,并将 numfree 自增 1 if (state->numfree < PyList_MAXFREELIST && PyList_CheckExact(op)) { state->free_list[state->numfree++] = op; OBJECT_STAT_INC(to_freelist); } else #endif { // 否则将列表的内存释放掉 Py_TYPE(op)->tp_free((PyObject *)op); } Py_TRASHCAN_END }
我们知道创建一个 PyListObject 对象会分为两步,先创建 PyListObject 对象,然后创建底层数组,最后让 PyListObject 对象的 ob_item 字段指向底层数组的首元素。
同理,在销毁一个 PyListObject 对象时,会先释放 ob_item 维护的底层数组,然后在缓存池已满的情况下再释放 PyListObject 对象自身。
现在我们算是明白了缓存池的机制,本来在销毁列表时,要将它的内存释放。但因为缓存池机制,解释器并没有这么做,而是将它的指针放在了缓存池里,至于列表对象则依旧驻留在堆上,只是我们已经无法再访问了。
当以后创建新的 PyListObject 对象时,解释器会首先唤醒这些已经死去的 PyListObject 对象,给它们一个洗心革面、重新做人的机会。但需要注意的是,这里缓存的仅仅是 PyListObject 对象,对于底层数组,其 ob_item 已经不再指向了。
从 list_dealloc 中我们可以看到,PyListObject 对象的指针在放进缓存池之前,ob_item 指向的数组就已经被释放掉了,同时数组中指针指向的对象的引用计数会减 1。所以最终数组中这些指针指向的对象也大难临头各自飞了,或生存、或毁灭,总之此时和 PyListObject 之间已经没有任何联系了。
但是为什么要这么做呢?为什么不连底层数组也一起维护呢?可以想一下,如果继续维护的话,数组中指针指向的对象永远不会被释放,那么很可能会产生悬空指针的问题。
但实际上是可以将底层数组进行保留的,做法是只将数组中指针指向的对象的引用计数减 1,然后将数组中的指针都设置为 NULL,不再指向之前的对象,但并不释放底层数组本身所占用的内存空间。这样一来释放的内存不会交给系统堆,那么再次分配的时候,速度会快很多。但这样会带来两个问题。
1)这些内存没人用也会一直占着,并且只能供 PyListObject 对象的 ob_item 指向的底层数组使用。
2)基于缓存池获取的列表的容量,和新创建的列表的容量不一定匹配。比如底层数组长度为 6 的 PyListObject * 被放入了缓存池,那么表示列表最多容纳 6 个元素,但如果我们要创建一个长度为 8 的列表怎么办?此时依旧要重新为底层数组申请内存。
因此基于以上两个原因,Python 选择将底层数组所占的内存交还给了系统堆,当然也节省了内存。
lst1 = [1, 2, 3] print(id(lst1)) # 139671899367680 # 扔到缓存池中,放在数组的尾部 del lst1 # 从缓存池中获取,也会从数组的尾部开始拿 lst2 = [1, 2, 3] print(id(lst2)) # 139671899367680 # 因此打印的地址是一样的
以上就是列表的创建和销毁,以及它的缓存池原理。
trashcan 机制
在看列表的销毁过程时,我们注意到里面有这么一行代码。
这是做什么的呢,首先在 Python 中,我们可以创建具有深度递归的对象,比如:
L = None for i in range(2 ** 20): L = [L] del L
此时的 L 就是一个嵌套了 2 ** 20 层的列表,当我们删除 L 的时候,会先销毁 L[0]、然后销毁 L[0][0],以此类推,直到递归深度为 2 ** 20。
而这样的深度毫无疑问会溢出 C 的调用栈,导致解释器崩溃。但事实上我们在 del L 的时候解释器并没有崩溃,原因就是 CPython 发明了一种名为 trashcan 的机制,它通过延迟销毁的方式来限制销毁的递归深度。关于这一特性,我们知道就好了,不用太关注。
下一篇文章来聊一聊列表的操作在底层是怎么实现的。