到目前为止,我们将 Python 管理的连续内存块设置为 4K 的 pool,恰好为多数操作系统的内存分页尺寸,这可以大大的提高操作系统的内存管理性能。如何充分利用这 4K 的内存池就是目前要考虑的问题了,一种简单的做法是在这块内存池里就地分配需要的空间,当然分配大小不可能超过 4K,由于 Python 只会在内存池中分配小雨 512 Bytes 的小对象,这是没问题的,但是随着内存的随机申请、归还,不久之后内存池就 “体无完肤” 的碎片化了,此路不通!所以 Python 将 4K 内存池分割成一系列固定大小的 block,那么 Python 所有对象都只能分配成相同的尺寸吗?当然不是了,Python 又创建了大量的内存池,它们的 block 尺寸大小不一,这样就可以按需使用了。需要什么 size 的内存,就去这种 size 对应的内存池里申请一个 block 就好了。对了,希望你还记得上一节提到的,内存池的 block size 序列是 8 * N(N >= 1).
内存池的模型
在开始之前,我们需要调整一下对 pool 结构的理解,之前对 pool 的描述更接近其在内存中的状态,实际上 pool 被切割成一系列 block 之后,每次内存分配都不会超出一个 block 的范围,所以 block 之间的顺序已经不那么重要了,这也是内存池经过一系列随机使用之后也不会碎片化的原因,毕竟,内存本身就是 block 大小的 “碎片” 不会被改变。那么在一个内存池中,实际上我们关心的是一个一个 block 的生死存亡问题,它们彼此之间虽然只在比邻之间,但从来不会有什么瓜葛。
图中所有内存块都是连续存在与内存池整个 4K 空间中,但是我将内存分为 3 类:
- pool header,位于内存池头部;
- freeblock,指向可复用的碎片内存;
- nextoffset 指向的 ”童子“ 内存,这些 block 没有被使用过,源码注释中称 virgin blocks。
当前图中忽略了内存池中一些无关的头部信息。图中用 block 和虚线箭头指明了这个内存池中的 block,所有 block 大小都一致,并且都是 8 Bytes 的整数倍。
上面图示的是内存池刚刚创建时的样子。
freeblock 是两面派
注意左侧 freeblock 指向的 block,这是一块当前内存池对外可以分配的内存,在 Python 的实现中,block 有两种形态:
- free block 链表节点;
- 通用内存;
当 block 被申请后,当时是被当作通用内存使用。当 block 在 freeblock 链表中时,block 的头部会储存一个 block 指针,指向下一个可用 block,如上图所示,内存池创建之初 freeblock 链表只有一个元素,那么它的 next 指向就是 Null,表示没有下一个节点。经过一段时间的使用,一些内存被归还到内存池会的时候,会被插入 freeblock 链表。
其实现在:
//Objects/obmalloc.c:1661
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
最新释放的内存会被插入 freeblock 的头部,在被插入 freeblock 链表之前,无论这个 block 有什么内容,其头部都会被链表的下一节点指针覆盖。