这篇文章描述python最新开发版(3.9.0a0)的内存管理机制,如果大家对GC回收感兴趣,也可以持续关注本人,后续将出相关的文章。
大家都知道在python中一切皆对象,其实这描述存在一定的问题,因为python是用c语言编写的,所谓的“对象”其实是用结构体来定义的,咱们暂且不去纠结这些刻板的问题。python中一些对象的定义将会依赖其他对象的定义。由于python具有动态语言这个特性,经常会分配一些内存占用非常少的对象。为了提高内存操作的效率和减少内存碎片,python底层开发了一套通用的内存管理机制,称为PyMalloc。
在讲解之前先看一下内存架构,让大家有宏观的印象。如下图所示:
为了印证我不是在这里卖弄,咱们可以在源代码中找找证据。
python对大内存(512字节为分界线)和小内存的处理方式就分道扬镳了。对于大内存python使用C Library标准库函数malloc去处理,对于小内存python开发的一套专门管理、分配内存的机制去处理。小内存管理分为三层,arena(区域),pool(池)和block(块)。
让我们从最下的block(块)开始讲起。
block(块)在内存中的大小是确定的,每个block(块)只能容纳一个python对象。block(块)的大小从8个字节到512个字节不等,但是必须是8字节的倍数。如下图所示:
pool(池)是指由一系列相同大小block(块)构成的集合。通常情况下pool(池)的大小是一个内存页(4kb),限制pool(池)的大小有助于减少碎片化。如果一个python对象被销毁了,内存管理器会用一个同样大小的对象填充这片区域。
每一个pool(池)都会有一个特殊的结构体头,如下图所示:
从上面代码可以看出,相同大小的pool(池)通过双向链表(nextpool和prepool字段)。szidx字段保存pool(池)分类索引。ref.count保存的是当前pool(池)中block(块)的已使用数量。arenaindex字段保存pool(池)在arena(区域)中的索引号。
freeblock的描述如下:
所以,一个不含有python对象的block(块)储存另外一个block(块)的地址,这个技巧节省了大量的内存和计算量。
每个pool(池)有3中状态:
- 已使用 部分使用,既不空也不满。
- 满 所有pool(池)中的block都已分配。
- 空 pool(池)中的block(块)都未被分配。
为了有效的管理pool(池)python定义了一个全局数组usedpools。它储存了分类的pool(池)对象指针。正如我们所知所有的大小相同的pool(池)对象链接在一起。为了迭代,我们必须知道链表的头。如果在usedpools中没有当前尺寸的pool(池),将会在内存中新申请。
需要注意的是pool(池)和block(块)不会从内存中直接分配,而是从已分配的arenas(区域)中直接分配。
arena是指在堆上分配的一个(可分配64个pool(池))256kb大小的内存。
arena(区域)的结构如下所示:
到此,小对象内存管理的大致脉络想必大家应该就清楚了吧。
最后讲讲内存销毁。
Python小对象管理器很少将内存直接使用完返回给操作系统。
如果所有的pool(池)为空,arena(区域)将会全部释放内存。这种情况会发生在在短期内分配了大量的临时对象。而由于这种内存管理机制,python进程中也会有一部分并未使用的内存。
大家还可以使用sys._debugmallocstats()对已使用的内存进行统计。
如果大家任何疑惑,欢迎跟我交流。