我读
对于Python,if(x)是存储x的内存地址。
并且它是一个给定的对象的id
永远不会改变,这意味着一个对象总是在其生命周期中存储在给定的内存地址。这导致了一个问题:(虚拟)内存碎片怎么样?
假设一个对象A
位于地址1(有id
1),占用10个字节,所以占用地址1-10。对象B
有id
11并占用字节11-12,对象C
占用地址13-22。一旦B超出范围并获得GC,我们就会出现碎片。
这个难题是如何解决的?
CPython为小对象pymalloc-allocator使用自己的内存分配器。在code itself中可以找到相当不错的描述。
这个分配器非常擅长避免内存碎片,因为它有效地重用了释放的内存。但是,它只是一种启发式方法,可能会出现导致内存碎片的情况。
让我们来看看当我们分配大小为1byte的对象时会发生什么。
CPython有自己的所谓竞技场,用于小于512字节的对象。显然,1字节请求将由其分配器管理。
请求的大小分为64个不同的类:0级用于大小为1..8字节,1级用于大小或9..16字节,依此类推 - 这是由于需要对齐8个字节。上述每个类都有自己或多或少的独立/专用内存。我们的要求是0级。
我们假设这是对这个size-class的第一个请求。将创建一个新的“池”或重用一个空池。池是4KB big,因此有512个8字节“块”的空间。尽管请求只有1个字节,但我们将阻塞占用块的另外7个字节,因此它们不能用于其他对象。所有空闲块都保存在列表中 - 开头所有512个块都在此列表中。分配器从该空闲块列表中删除第一个块,并将其地址作为指针返回。
池本身标记为“已使用”,并添加到第0类的已使用池列表中。
现在,分配另一个大小<= 8字节的对象发生如下。首先,我们查看第0类的已使用池列表,并找到一个已经在使用的池,即有一些已使用的池和一些空闲块。 Allocator使用第一个空闲块,将其从空闲块列表中删除,并将其地址作为指针返回。
删除第一个对象很简单 - 我们将被占用的块添加为(到目前为止单个)已使用池中的空闲块列表的头部。
当创建一个8字节的新对象时,使用free-block-list中的第一个块,这是第一个现在删除的对象使用的块。
正如您所看到的,内存被重用,因此内存碎片大大减少。这并不意味着不存在内存碎片:
在分配512个1字节对象之后,第一个池变为“满”,并且将创建/使用用于第0类大小的新池。我们还添加了另外512个对象,第二个池也变为“满”。等等。
现在,如果删除前511个元素 - 仍然会有一个字节阻塞整个4KB,不能用于其他类。
仅当释放最后一个块时,池才变为“空”,因此可以重用于其他大小类。
空池不会返回到操作系统,而是留在竞技场中并重复使用。然而,pymalloc manages multiple arenas,如果竞技场变得“未使用”,它可能被释放并且占用的存储器(即池)被返回到OS。