Python 对内存进行了多层抽象:
- 0 层就是最基本的 C 库,我们熟悉的 malloc、free 等函数就在这一层;
- 1 层是对 C 库内存管理接口的封装,基本上只是对 malloc、free 的代理函数,主要是处理 size 为 0 的时候,不同系统的差异性,Python 的策略也非常简单,当 size 为 0 的时候,至少申请 1 字节内存,这样就解决了不同系统之间的差异性;
- 2 层实现了 Python 高级内存管理系统,这也是我们研究的重点;
- 3 层就到了对象层,这一层主要是一些针对对象的缓存优化,比如前面提到的小整数数组、拉丁字母表等技术。
我们采用从下向上的研究思路,最底层的 malloc 和 free,只要有过 C 开发经验就会很熟悉。
第 0 ~ 1 层代码非常简单,代码如下:
static void *
_PyMem_RawMalloc(void *ctx, size_t size)
{
if (size == 0)
size = 1;
return malloc(size);
}
static void *
_PyMem_RawCalloc(void *ctx, size_t nelem, size_t elsize)
{
if (nelem == 0 || elsize == 0) {
nelem = 1;
elsize = 1;
}
return calloc(nelem, elsize);
}
static void *
_PyMem_RawRealloc(void *ctx, void *ptr, size_t size)
{
if (size == 0)
size = 1;
return realloc(ptr, size);
}
static void
_PyMem_RawFree(void *ctx, void *ptr)
{
free(ptr);
}
可以证实它们都只是对 C 库最简单的封装,在 Python 2.5 版本中,甚至就是用宏实现的。那么让我们直接向上一层研究。
内存管理的主要目的:
- 首先,避免内存泄漏;
- 提高内存分配效率。
避免内存泄漏的办法很多,其中一种比较朴素的办法是将所有已申请的内存加入到链表中,释放时可以确保全部释放。可以参考很久以前实现的一个简单版本(https://github.com/alvendarthy/tree_style_mem_pool)。
这种做法确实可以解决内存泄漏的问题,但内存分配依然使用 malloc 和 free,程序长时间运行后,内存碎片化比较严重,没有解决内存分配效率的问题。
提高内存跟配效率常用的做法是,一次性申请一块大的内存由应用自行管理,应用需要内存时不使用 malloc,而从这块内存中划出所需空间,释放的时候也不调用 free,而是将不再需要的内存归还给内存池。常见的内存管理技术大体思路都与这个差不多,差异主要表现在对内存池的管理环节。
在 Python 中无论通过 PyObject_GC_New 还是直接用较低级接口 PyObject_MALLOC,都不会直接穿透到第 1 层,通常由下面这样的接口处理内存申请:
static void *
_PyObject_Malloc(void *ctx, size_t nbytes)
{
void* ptr = pymalloc_alloc(ctx, nbytes);
if (ptr != NULL) {
_Py_AllocatedBlocks++;
return ptr;
}
ptr = PyMem_RawMalloc(nbytes);
if (ptr != NULL) {
_Py_AllocatedBlocks++;
}
return ptr;
}
其中 pymalloc_alloc 是从 Python 的内存池中划分空间,PyMem_RawMalloc 实际上就是 C 库中的 malloc 分配空间。Python 决定内存分配来源的条件通过 pymalloc_alloc 的返回值表示,当其返回 Null 时表示出错或者空间大小不符合内存池分配条件,就会转而尝试直接使用 C 库分配。在 pymalloc_alloc 中由这样的判断:
if (nbytes == 0) {
return NULL;
}
if (nbytes > SMALL_REQUEST_THRESHOLD) {
return NULL;
}
显然,如果分配空间大小为 0,或者大于 SMALL_REQUEST_THRESHOLD 就转由 malloc 申请内存。在 Python 3.8 中 SMALL_REQUEST_THRESHOLD 是一个宏,其数值是 512,Python 2.5 中是 256,看来新版本扩大了内存池的边界,应该是计算机内存增长的结果。
这样情况就很清楚了,只有小于 512 字节的对象才会在内存池中分配,0 或者 大于 512 字节的对象会直接使用 C lib 接口,直接从系统获得内存.
明天我们继续深入,研究这些小对象在内存中的创建和管理。