我,子牙老师,一个手写过操作系统、编程语言、Java虚拟机、docker、Ubuntu系统...的硬核男人
最近抽空研究了CPython的内存机制,写篇文章记录一下。看完本篇文章,Python虚拟机CPython内存管理机制的源码,你就能轻松看懂
本篇文章,你会看到:
- CPython的内存分配域
- pymalloc中的arena、pool、block之间的关系
- arena、pool的设计细节
- usedpools的设计细节及N问题解答
- 创建对象时,CPython是如何从pymalloc中分配内存的
以下,enjoy
内存分配域
CPython中定义了三种内存分配域(domain)来支持不同用途的内存管理机制

虽然定义了三个内存分配域,事实上只实现了两个:Raw Domain、Object Domain。Mem Domain使用的也是Object Domain
如果不开启WITH_PYMALLOC,那三个内存分配域,使用的就是同一个:Raw Domain

WITH_PYMALLOC的用途:控制是否启用 Python 自带的小对象内存池分配器pymalloc
这个配置默认是开启的,如果你想查看

如何禁用呢?

Object Domain与pymalloc是什么关系呢?Object Domain专用于 Python 对象,由 pymalloc 管理,分配小对象。那pymalloc底层是如何玩的呢?
pymalloc
pymalloc底层是通过三个玩意来管理的:arena、pool、block。理解了这三者之间的关系,你就把pymalloc玩明白了。它们的关系如图

arena,一块256K的内存,由arena_object管理,属性address指向这块内存区域
初始的时候,pool_address的值与address相同,但是address是固定不变的,pool_address会随着arena中的内存使用而改变。比如最开始是指向第一个pool,第一个pool被使用了,就会指向第二个pool,言外之意就是它永远指向下一个可用的pool
一个pool,4K。所以一个arena,有64个pool。那一个pool中有多少block呢?看情况
64位系统中,block大小从16B开始,步长也是16,终点是512B,一共是32种:16、32、48、64...512
所以一个block,如果是16B,那就是4096/16=256个。真的是这样吗?不是!因为一个pool,4K内存,它的头部会放这个pool的管理数据,即pool_header,占48B,所以结果是(4096-48)/16=253
虽然启用了pymalloc,但是超过512B的内存申请不会走pymalloc,还是会走Raw Domain。即使用c库函数malloc、calloc向系统申请内存

我现在讲的这些都是后面看懂代码的关键,所以没看懂的多看几遍
这三者之间的大体关系差不多就是这样,接下来举个例子帮助大家彻底理解
pymalloc_alloc
当我们需要分配内存的时候,整个调用链是这样

看下pymalloc_alloc的源码

第一个判断:如果要分配的内存大小是0,返回NULL
第二个判断:如果分配的内存大小超过512B,返回NULL,就回到调用函数中,走RawMalloc,向OS要内存

再将nbytes转成size class,即图中的i,去usedpools中找到对应的pool,从中分配内存

如果要分配的内存是1B-16B,对应的size class=0;如果要分配的内存是17B-32B,对应的size class=1......
494行代码是不是挺奇怪的?你可能想问:为什么不是usedpools[size]呢?等下讲。这个明白了,498行代码也就明白了:为什么不是判断pool==NULL
第一次分配内存,会走到516行,函数allocate_from_new_pool里面干了什么,等下讲
下面解决第一个问题:usedpools
usedpools
usedpools的初始代码是很难理解的

在64位系统中,NB_SMALL_SIZE_CLASSES=32,宏展开以后长这样

第一个问题是:usedpools数组的所有值为什么不设置为NULL,而是给它赋初值PT(x)?

答案:为了实现环形哑链表。这个链表比起普通的链表有什么好处?不需要判断空指针
usedpools的所有值被称为dummy pool
第二个问题:usedpools[2x]与usedpools[2x+1]的值都是相等的,为什么?这也是一种代码设计策略

其中usedpools[2x]对应dummy pool的next,usedpools[2x+1]对应dummy pool的prev。你如何论证呢?看代码

第三个问题:- 2*sizeof(block *),在64位系统中相当于-16,next在pool_header中的offset是16,这个没错,但是prev在pool_header中的offset是24,为什么也是-16?因为初始的时候只要保持非NULL即可
第四个问题:usedpools[2x]为什么不直接指向dummy pool,而是dummy pool的next?ChatGPT这样说
至此,usedpools你就算玩明白了
allocate_from_new_pool
理解了上面这些,你就能轻松看懂CPython内存管理机制相关源码,不信咱们来试试

刚开始,usable_arenas中还没有数据,就调用new_arena创建,初始创建16个,后面每次double倍创建

从arena中拿一个pool出来用,并做好相关的值设置。如果这个pool是arena中的最后一个,就切换到下一个arena待用

完成pool的初始化

头插法,将pool插入usedpools对应的size class链表中

如果usedpools中有对应的size class对应的pool,直接拿出来用

如果usedpools中没有对应的size class对应的pool,从前面拿出来用的pool中分配block并返回
这里注意一点:struct arena_object与256K内存是分配的,但是struct pool_header是包含在4K内存之中的,所以一个pool真正可用的内存大小是4K-sizeof(pool)
至此,Python虚拟机CPython的内存管理机制,你就算过关了。下一篇,聊Python虚拟机的GC机制。关注公众号**【硬核子牙】**,看硬核文章