Python之内存管理
之前分析过C++中STL的内存管理,在读完《Python源码剖析》之后决定再来总结一下Python中的内存管理机制,以进一步体会内存管理这个主题。
一般内存管理都要涉及到小块内存的管理,这点一般都采用内存池来实现,而另一方面,是如何归还向系统申请的内存,这一点C++是通过RAII(Resource Acquisition Is Initialization)来进行的,也即是资源分配即初始化,保证在任何情况下,先构造对象,再析构对象,是一种基于对象的资源管理办法,建立在C++对构造函数、析构函数、拷贝函数的基础之上(见effective c++条款13),而Python的内存管理则采用和Java类似的垃圾回收机制(Garbage Collection),这里暂不讨论RAII和GC孰优孰劣(个人更倾向于RAII,因为它是确定的,而且“对象在,资源在,对象没,资源没” 的理念难道不是更符合认知吗?)。下面仅仅分析Python中比STL中更为复杂和精巧的内存池的实现。
内存管理架构
先看一下Python的内存管理架构,Python中内存管理分为四个层次:
- 最底层的C函数库层(malloc, free)
- PyMem_API,对C内存管理函数进行了一层封装,以兼容各个平台,提供可移植性
- PyObj_API,提供更高抽象层次的内存管理接口,以PyObject_Malloc,PyObject_Realloc,PyObject_Free示人
- 针对不同类型对象(int,string,list,dict…)的对象缓冲池机制
Python中的对小内存区块的管理大部分工作就是实现PyObj_API这一层接口,相当于STL中的第二级配置器,而PyMem_API就是STL第一级配置器。
内存池
内存池全景
Python源码剖析(本文所有图均截自此文献)给出了Python处理小块内块的内存池全景,如下图所示(看起来好复杂-.-):
可以从图中看到主要包括三层结构和一个usedpool缓冲池,由下到上分为block,pool,arena。pool管理着一堆block,arena管理着一堆pool。
block
block内存配置的最小单位,容量是8的整数倍,最小为8字节,最大为256字节的内存块,也就是说Python中对于256字节以下的采用PyObj函数族,256字节以上的交给PyMem函数族。这点和STL中类似(STL中用数组维护了类似的链表,最大为128字节)。
pool
pool管理着一堆固定大小的内存块block,一个pool的大小通常是一个系统内存页(一般为4KB),其结构定义如下:
需要注意的pool_header和其所管理的内存是连续的,下面是其结构图:
对于这个结构其实很好理解,每次分配一个block出去,更新偏移量和freeblock直到到达最大偏移量。其实这个结构中最巧妙的就是freeblock指针,它是用来指向pool中一个未被使用的自由的block,关键是内存释放之后会导致pool中出现大量离散的自由block,Python必须建立一种机制将这些离散的自由block组织起来,再次使用。Python就是利用这个freeblock指针实现一个离散自由block链表。那么如何实现的呢?其实考虑到未分配的block其实还可以用作其他用途,即存储其下一个未分配的block的地址Σ(っ °Д °;)っ!那么freeblock就是指向这个离散自由链表的表头~,这种思想可以说在Python实现中处处可见。可以看一下释放时的代码:
这样,产生的就是如下图所示的离散自由freeblock链表:
arena
一个arena管理多个pool,默认管理大小为256KB,一般也就是256/4=64个pool,其arena_object结构体如下:
python运行时会维护一个arena_object的集合(数组),在此数组上又维护了两类链表(这种技巧经常出现啊~~~),一种是未使用的unused_arena_objects单链表(未与pool建立联系), 另一种是可用的usable_arenas双向链表(已与pool建立联系),需要注意的是在配置arenas的内存时并没有配置其管理的pool的内存,只有当从未使用的unused_arena_objects单链表中取出一个变成可用状态时才会分配ARENA_SIZE(256KB)的内存。
usedpool数组
arena集合是小块内存池,但是当Python申请内存时,最基本的操作单元并不是arena而是pool, pool一共有三种状态:
- used状态:pool中至少有一个block已被使用,由usedpool数组维护
- full状态:所有的block都被使用了,这种状态的pool在arena中,但不在arena的freepools链表中
- empty状态:pool中所有的block都未被使用,处于这个状态的pool通过pool_header中的nextpool构成一个链表,这个链表的表头是arena_object中的freepools
这个usedpool数组也非常精巧,索引下标sizeindex是对于大小的pool,数组初始的元素值是指向前面8个字节(如下图所示),当判断特定sizeindex为 i 的pool是否有可用数组时 直接判断是否 usedpool[i+i]->nextpool == pool,如下图所示:
总结
这里只分析了内存池里各个数据结构,其实内部如何实现配置内存,释放内存,以及pool各个状态的转换,各式各样的链表的维护都是非常非常复杂,精细和繁琐的….关键还是用C开发的……..真是应了那句话,细节是魔鬼!
本文大部分参考了Python源码分析,这是一本好书!