CPython内存模型概念
有了前面的基础,本篇我们从一个更高的思维视角来鸟瞰一下CPython实现的内存管理模型,下图是Python内存体系结构各层的介绍
- 第3层是Python内置的基本类型(list,dict,list,str)的等内置类型的对象内存分配CPython为它们实现了专有分配方案(例如:int使用简单的自由列表),而其他。这也是周期垃圾回收器对容器对象进行选择性操作的地方。以及直接和C底层运行库交互CPython内核代码所需的内存空间。
- 第2层是Python对象分配器,主要职责为通用Python对象分配/释放内存(例如:PyObject_New / Del)都会调用它。
- 第1层是PyMem内存分配函数,它确保CPython运行时,堆中有足够可用的堆内存,如果没有,它会向下一层请求更多的内存。
- 第0层是C库中的malloc函数直接和操作系统的虚拟内存管理器交互,并且接受来自上一层PyMem内存分配函数的内存空间请求,以及将系统分配的内存空间返回给上一层PyMem。
- 第-1层是涉及OS底层的虚拟内存管理原理,超出了Python内核话题的范畴,故不作讨论。

了解上面这个图对我们深入认识CPython内存管理有非常思维导向作用。然后我们可以分散地逐层去剖析过中细节啦。
浅谈Python对象的本源
从前面的文章,我们已经理解到CPython实现中,一切事物的本源都是对象,即C底层的代码细节,它位于CPython源代码的Include/object.h中的第105行开始
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt; //引用计数器
PyTypeObject *ob_type;
} PyObject;
_PyObject_HEAD_EXTRA表示每个PyObject的内部就有两个指针指向PyObject*指针类型的字段,_ob_next表示指向当前对象的下一个Python对象,_ob_prev表示指向当前对象的前一个Python对象。

那么,在CPython运行时系统中,堆中的所有Python对象都构成了一个双向链表,下图是一个简化的逻辑形式,使用双向链表的目地是为了加快CPython的Python对象查找操作。因为双向链表比单向链表,在很多情况下使Python对象查找的时间开销缩短至少50%。

PyTypeObject是 struct _typeobject的类型别名,代码细节位于源文件Include/object.h的193行开始,该字段ob_type事实上定义Python对象的所有细节,例如
- 堆内存分配的尺寸,也就是什么类型
- 类型名称
- 该对象的方法
- 该对象的属性
- 对象文档描述
- 子类型列表
目前,PyTypeObject类型字段不是我们本文关心的

py_ssize_t ob_refcnt 这是引用计数器,那么以下对Python对象的描述都是正确的
- 任何Python对象都位于堆内存中,任何Python对象的引用位于栈中.
- 任何Python对象内部都由一个计数器
内置的Python数据类型有自己的特定于对象(Object-specific)的内存分配器,该分配器知道如何获取用于存储该对象的内存。 每个对象还有一个特定于对象的内存释放器(Memory deallocator),可在不再需要该内存时“释放”该内存,例如运行时字符串驻留问题,小型数字类型的内存预分配,以及其他容器的内存分配问题,我会在以后开篇谈及,这里不做展开讨论。
CPython的内存分配策略
CPython的内存管理策略,分3个不同级别的对象,分别是Arenas->pool->block,我先用一个思维导图,让你脑海中建立这三个对象的层次关系,读者可以先通过下图来初步理解这三个对象。

-
每个Arenas对象包装包含64个内存池,每个Arenas固定大小为256KB,并且该对象头部用两个struct area_object类型的指针在堆中构成Arenas对象的双重链表。
-
每个内存池(Pool),固有尺寸为4KB,每个内存池包含尺寸相同的逻辑块,并且并且该对象头部用两个struct pool_header类型的指针构成pool对象的双重链表。
-
块是封装Python对象的基本单位,对于Areas对象来说都按8字节的块来划分PyMem已分配的所有堆内存(备注:切入点1)。
块(Block)
在arenas内存管理对象首先定义逻辑上的“块”,并且用8字节对齐的方式确定块的尺寸,换句话说块的尺寸可以看作8的倍数那么大,例如你创建来一个25字节的Python对象,25字节不是8字节的倍数,那么CPython运行时系统会根据内存对齐的原则为该Python对象额外添加7个填充字节,就凑够32字节(8的倍数),更明确地说,对于一个实际尺寸位于25~32字节这个区间的任意Python对象,都能放入一个32字节的逻辑块中

对于在512字节以内的任意Python对象,CPython要在某个池中找到特定尺寸的逻辑块,此时需要一个引入叫size index,由于所有块的尺寸是8字节对齐,索引的计算公式非常简单,块索引=(已分配内存尺寸 / 8)-1,这里用到C/C++内存对齐的基础知识
不过在深入了解这个CPython的内存策路前,我们需要引入两个CPython的专业术语,CPython根据内存分配的尺寸的阀值512字节可以分为,对Python对象做如下分类:
-
大于512字节的Python对象,称为大型对象(Big),而Arenas对象的尺寸为256KB就是CPython中大型对象因此Arenas对象的内存分配,CPython会选择调用PyMem_RawMalloc()或PyMem_RawRealloc()为其分配内存,换句话就是通过第0层去调用C库的malloc分配器,因此C底层的malloc分配器是仅供给arenas对象使用的。
-
少于或等于512字节的Python对象,称为小型对象(Small),小型对象的内存请求按该对象的类型尺寸分组,这些分组按8个字节对齐,由于返回的地址必须有效对齐。这些类型尺寸的对象的内存请求由4KB的内存池提供内存分配,当然前提是该内存池有闲置的块。
基于类型尺寸的内存分段
内存池将块按需分段,因此池内部包含一个[特定类型尺寸]的块的空闲链表(备注:切入点2)。换句话说,每个类型尺寸有一个固定大小的内存分配器。空闲池由不同的(类型尺寸)的分配器共享,从而最大程度地减少了为[特定类尺寸]保留的空间。
CPython的内存分配策略,基于上文提到两个切入点,这种分配策略是所谓“基于空闲链表(数组)的简单分段存储”的一种变体。简单分段存储的主要缺点是,我们可能最终会为不同的空闲列表保留大量内存,这些空闲列表在时间上并没有得到充分利用。为了避免这种情况,我们在池中对每个空闲列表进行分区,并在所有空闲列表之间动态共享保留的空间。这种技术对于主要分配小型对象的内存密集型程序非常有效。对于小型对象的内存请求,我已经在上面的小型对象的内存分配表一一列明。
我们详细讨论了块和Arenas对象和内存池对象的关系,那么我们应该深入了解一下池和Arenas,还没有提到的其他特性。
池(Pools)
池封装大小相同的块,每个池的大小为4KB,每个池对象都有一个overhead的头部,其中包含闲置块(freeblock)的列表,每个池对象凭借头部的*nextpool和prevpool这两个 struct pool_header指针分别指向当前池对象下一个内存池和上一个内存池,我们可以查看CPython源代码的/Objects/obmalloc.c从938到948行的池头部定义,这里我们集中关注pool_header结构体中 freeblock字段,它是一个block类型的指针
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};
Python对象被封装为8字节对齐的块,带来的好处是能更好地管理闲置的内存块,当小型的Python对象不再有任何外部Python代码引用时,CPython的Arenas内存管理对象并没有立即将这些内存返回给操作系统,而是将这些用过的块空间标记为闲置,并在该小型Python对象释放后,将被标记为已使用(Used,请留意我这里使用的字眼)的块插入(链接)到pool对象头部的空闲块列表(freeblock)
下图是单个内存池的示意图,红色的表示已用的块,绿色标识闲置的块。

你可能会问,又闲置又已使用,不是矛盾吗?我们看看内存池中块集合的三种状态,
- 已使用:块集合既不为闲置的块也有已使用的块(红色的),并且当前已使用的块至少一个以上
- 满载:表示当前没有可用的块
- 空载:表示每个块都是空的,

对于上面的三种块集合的状态,只有空载状态会被链接到Arena对象的空闲池(freepools)链表
Arenas内存管理对象
arenas用于封装内存池,和内存池一样也有一个头部开销用于跟踪内存池链表,arenas对象有如下行为参数
- 其他抽象的内存管理对象将使用arena对象对象的空间
- 仅当arena对象的所有池为空载时,CPython将该对象占用的内存空间释返还给操作系统。
查看CPython源代码Objects/obmalloc.c文件,如下

更新待续.....
网友评论