CPython 对小对象使用自己的内存分配器,pymalloc 分配器 https://docs.python.org/3/c-api/memory.html#the-pymalloc-allocator。可以在以下位置找到非常好的描述代码本身 https://github.com/python/cpython/blob/8905fcc85a6fc3ac394bc89b0bbf40897e9497a6/Objects/obmalloc.c#L741.
这个分配器非常擅长避免内存碎片,因为它有效地重用了释放的内存。然而,这只是一种启发式方法,人们可能会想出导致内存碎片的场景。
让我们看看当我们分配一个大小为 1 字节的对象时会发生什么。
CPython 对于小于 512 字节的对象有自己的所谓的 arena。显然,1 字节请求将由其分配器管理。
请求的大小分为 64 个不同的类别:第 0 类适用于 1..8 字节的大小,第 1 类适用于 9..16 字节的大小,依此类推 - 这是由于需要 8 字节对齐。上述每个类都有自己或多或少的独立/专用内存。我们的要求是 0 级。
我们假设这是对此尺寸级别的第一个请求。将创建一个新的“池”或重新使用一个空池。一个游泳池是4KB big https://github.com/python/cpython/blob/8905fcc85a6fc3ac394bc89b0bbf40897e9497a6/Objects/obmalloc.c#L867,因此有 512 个 8 字节“块”的空间。尽管只请求 1 个字节,但我们将阻塞占用块的另外 7 个字节,因此它们不能用于其他对象。所有空闲块都保存在一个列表中 - 一开始所有 512 个块都在此列表中。分配器从该空闲块列表中删除第一个块,并将其地址作为指针返回。
该池本身被标记为“已使用”,并添加到 0 级已用池列表中。
现在,分配另一个大小
删除第一个对象很容易 - 我们将占用的块添加为(到目前为止单个)已使用池中的空闲块列表的头部。
当创建 8 字节的新对象时,将使用空闲块列表中的第一个块,这是第一个现已删除的对象使用的块。
正如您所看到的,内存被重用,因此内存碎片大大减少。这并不意味着不能存在内存碎片:
分配 512 个 1 字节对象后,第一个池将变“满”,并且将创建/使用第 0 类大小的新池。一旦我们添加了另外 512 个对象,第二个池也将变得“满”。等等。
现在,如果删除前 511 个元素 - 仍然会有一个字节阻塞整个 4KB,无法用于other类。
仅当最后一个块被释放时,池才变为“空”,从而可以重新用于其他大小类别。
空池不会返回给操作系统,而是留在竞技场中并被重用。然而,pymalloc管理多个竞技场 https://github.com/python/cpython/blob/8905fcc85a6fc3ac394bc89b0bbf40897e9497a6/Objects/obmalloc.c#L1074,如果一个 arena 变得“未使用”,它可能会被释放,并将占用的内存(即池)返回给操作系统。