Python虚拟机内存机制底层

我,子牙老师,一个手写过操作系统、编程语言、Java虚拟机、docker、Ubuntu系统...的硬核男人

最近抽空研究了CPython的内存机制,写篇文章记录一下。看完本篇文章,Python虚拟机CPython内存管理机制的源码,你就能轻松看懂

本篇文章,你会看到:

  1. CPython的内存分配域
  2. pymalloc中的arena、pool、block之间的关系
  3. arena、pool的设计细节
  4. usedpools的设计细节及N问题解答
  5. 创建对象时,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机制。关注公众号**【硬核子牙】**,看硬核文章

相关推荐
肖永威2 小时前
macOS环境安装/卸载python实践笔记
笔记·python·macos
TechWJ3 小时前
PyPTO编程范式深度解读:让NPU开发像写Python一样简单
开发语言·python·cann·pypto
枷锁—sha3 小时前
【SRC】SQL注入WAF 绕过应对策略(二)
网络·数据库·python·sql·安全·网络安全
abluckyboy3 小时前
Java 实现求 n 的 n^n 次方的最后一位数字
java·python·算法
喵手3 小时前
Python爬虫实战:构建各地统计局数据发布板块的自动化索引爬虫(附CSV导出 + SQLite持久化存储)!
爬虫·python·爬虫实战·零基础python爬虫教学·采集数据csv导出·采集各地统计局数据发布数据·统计局数据采集
天天爱吃肉82184 小时前
跟着创意天才周杰伦学新能源汽车研发测试!3年从工程师到领域专家的成长秘籍!
数据库·python·算法·分类·汽车
m0_715575344 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
甄心爱学习4 小时前
【leetcode】判断平衡二叉树
python·算法·leetcode
深蓝电商API4 小时前
滑块验证码破解思路与常见绕过方法
爬虫·python
Ulyanov4 小时前
Pymunk物理引擎深度解析:从入门到实战的2D物理模拟全攻略
python·游戏开发·pygame·物理引擎·pymunk