内存管理

概述

Python 中的内存管理涉及一个私有堆,其中包含所有 Python 对象和数据结构。这个私有堆的管理由 Python 内存管理器 内部负责。Python 内存管理器有不同的组件,用于处理各种动态存储管理方面的问题,例如共享、分段、预分配或缓存。

在最低层,原始内存分配器通过与操作系统内存管理器交互,确保私有堆中有足够的空间来存储所有与 Python 相关的数据。在原始内存分配器之上,几个特定于对象的分配器在同一个堆上运行,并根据每种对象类型的特性实现不同的内存管理策略。例如,整数对象在堆中的管理方式与字符串、元组或字典不同,因为整数意味着不同的存储要求和速度/空间权衡。因此,Python 内存管理器将部分工作委托给特定于对象的分配器,但确保后者在私有堆的范围内运行。

重要的是要理解,Python 堆的管理是由解释器本身执行的,用户无法控制它,即使他们经常操作该堆内内存块的对象指针。Python 对象和其他内部缓冲区的堆空间分配是由 Python 内存管理器通过本文档中列出的 Python/C API 函数按需执行的。

为避免内存损坏,扩展作者绝不应尝试使用 C 库导出的函数来操作 Python 对象:malloc()calloc()realloc()free()。这将导致 C 分配器和 Python 内存管理器之间的混合调用,从而产生致命后果,因为它们实现了不同的算法并操作不同的堆。但是,可以安全地使用 C 库分配器为个人目的分配和释放内存块,如以下示例所示:

PyObject *res;
char *buf = (char *) malloc(BUFSIZ); /* for I/O */

if (buf == NULL)
    return PyErr_NoMemory();
...Do some I/O operation involving buf...
res = PyBytes_FromString(buf);
free(buf); /* malloc'ed */
return res;

在此示例中,I/O 缓冲区的内存请求由 C 库分配器处理。Python 内存管理器仅参与返回的字节对象的分配。

然而,在大多数情况下,建议从 Python 堆分配内存,特别是由于后者在 Python 内存管理器的控制之下。例如,当解释器用 C 编写的新对象类型进行扩展时,这是必需的。使用 Python 堆的另一个原因是希望 告知 Python 内存管理器扩展模块的内存需求。即使请求的内存完全用于内部、高度特定的目的,将所有内存请求委托给 Python 内存管理器也会使解释器更准确地了解其整体内存占用。因此,在某些情况下,Python 内存管理器可能会或可能不会触发适当的操作,例如垃圾回收、内存压缩或其他预防性过程。请注意,如上一个示例所示,通过使用 C 库分配器,I/O 缓冲区分配的内存完全逃离了 Python 内存管理器。

参见

PYTHONMALLOC 环境变量可用于配置 Python 使用的内存分配器。

PYTHONMALLOCSTATS 环境变量可用于在每次创建新的 pymalloc 对象区域时和关闭时打印 pymalloc 内存分配器 的统计信息。

分配器域

所有分配函数都属于三个不同的“域”之一(另请参见 PyMemAllocatorDomain)。这些域代表不同的分配策略,并针对不同的目的进行优化。每个域如何分配内存或每个域调用哪些内部函数的具体细节被认为是实现细节,但为了调试目的,可以在 此处 找到一个简化表。用于分配和释放内存块的 API 必须来自同一个域。例如,必须使用 PyMem_Free() 来释放使用 PyMem_Malloc() 分配的内存。

三个分配域是:

  • 原始域:旨在为通用内存缓冲区分配内存,其中分配 必须 交给系统分配器,或者分配器可以在没有 附加线程状态 的情况下运行。内存直接从系统请求。参见 原始内存接口

  • “Mem”域:旨在为 Python 缓冲区和通用内存缓冲区分配内存,其中分配必须在 附加线程状态 下执行。内存取自 Python 私有堆。参见 内存接口

  • 对象域:旨在为 Python 对象分配内存。内存取自 Python 私有堆。参见 对象分配器

备注

自由线程 构建要求仅使用“对象”域分配 Python 对象,并且所有 Python 对象都使用该域分配。这与之前的 Python 版本不同,在之前的版本中这只是一种最佳实践,而不是一项硬性要求。

例如,缓冲区(非 Python 对象)应该使用 PyMem_Malloc()PyMem_RawMalloc()malloc() 分配,但不能使用 PyObject_Malloc()

参见 内存分配 API

原始内存接口

以下函数集是系统分配器的包装器。这些函数是线程安全的,因此不需要 线程状态附加

默认原始内存分配器 使用以下函数:malloc()calloc()realloc()free();当请求零字节时,调用 malloc(1)(或 calloc(1, 1))。

在 3.4 版本加入。

void *PyMem_RawMalloc(size_t n)
自 3.13 版本起成为 稳定 ABI 的一部分。

分配 n 字节,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL

如果可能,请求零字节会返回一个不同的非 NULL 指针,就像调用了 PyMem_RawMalloc(1) 一样。内存将不会以任何方式初始化。

void *PyMem_RawCalloc(size_t nelem, size_t elsize)
自 3.13 版本起成为 稳定 ABI 的一部分。

分配 nelem 个元素,每个元素的字节大小为 elsize,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL。内存初始化为零。

如果可能,请求零元素或零字节大小的元素会返回一个不同的非 NULL 指针,就像调用了 PyMem_RawCalloc(1, 1) 一样。

在 3.5 版本加入。

void *PyMem_RawRealloc(void *p, size_t n)
自 3.13 版本起成为 稳定 ABI 的一部分。

p 指向的内存块大小调整为 n 字节。内容在旧大小和新大小的最小值范围内保持不变。

如果 pNULL,则调用等同于 PyMem_RawMalloc(n);否则,如果 n 等于零,则内存块大小已调整但未释放,并且返回的指针是非 NULL

除非 pNULL,否则它必须是由先前调用 PyMem_RawMalloc()PyMem_RawRealloc()PyMem_RawCalloc() 返回的。

如果请求失败,PyMem_RawRealloc() 返回 NULL,并且 p 仍然是先前内存区域的有效指针。

void PyMem_RawFree(void *p)
自 3.13 版本起成为 稳定 ABI 的一部分。

释放 p 指向的内存块,该内存块必须是由先前调用 PyMem_RawMalloc()PyMem_RawRealloc()PyMem_RawCalloc() 返回的。否则,或者如果之前已调用 PyMem_RawFree(p),则会发生未定义行为。

如果 pNULL,则不执行任何操作。

内存接口

以下函数集,以 ANSI C 标准为蓝本,但指定了请求零字节时的行为,可用于从 Python 堆分配和释放内存。

默认内存分配器 使用 pymalloc 内存分配器

警告

使用这些函数时必须存在 附加线程状态

3.6 版本变更: 默认分配器现在是 pymalloc 而不是系统 malloc()

void *PyMem_Malloc(size_t n)
作为 稳定 ABI 的一部分。

分配 n 字节,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL

如果可能,请求零字节会返回一个不同的非 NULL 指针,就像调用了 PyMem_Malloc(1) 一样。内存将不会以任何方式初始化。

void *PyMem_Calloc(size_t nelem, size_t elsize)
自 3.7 版本起成为 稳定ABI 的一部分。

分配 nelem 个元素,每个元素的字节大小为 elsize,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL。内存初始化为零。

如果可能,请求零元素或零字节大小的元素会返回一个不同的非 NULL 指针,就像调用了 PyMem_Calloc(1, 1) 一样。

在 3.5 版本加入。

void *PyMem_Realloc(void *p, size_t n)
作为 稳定 ABI 的一部分。

p 指向的内存块大小调整为 n 字节。内容在旧大小和新大小的最小值范围内保持不变。

如果 pNULL,则调用等同于 PyMem_Malloc(n);否则,如果 n 等于零,则内存块大小已调整但未释放,并且返回的指针是非 NULL

除非 pNULL,否则它必须是由先前调用 PyMem_Malloc()PyMem_Realloc()PyMem_Calloc() 返回的。

如果请求失败,PyMem_Realloc() 返回 NULL,并且 p 仍然是先前内存区域的有效指针。

void PyMem_Free(void *p)
作为 稳定 ABI 的一部分。

释放 p 指向的内存块,该内存块必须是由先前调用 PyMem_Malloc()PyMem_Realloc()PyMem_Calloc() 返回的。否则,或者如果之前已调用 PyMem_Free(p),则会发生未定义行为。

如果 pNULL,则不执行任何操作。

为了方便起见,提供了以下面向类型的宏。请注意,TYPE 指的是任何 C 类型。

PyMem_New(TYPE, n)

PyMem_Malloc() 相同,但分配 (n * sizeof(TYPE)) 字节的内存。返回一个强制转换为 TYPE* 的指针。内存将不会以任何方式初始化。

PyMem_Resize(p, TYPE, n)

PyMem_Realloc() 相同,但内存块大小调整为 (n * sizeof(TYPE)) 字节。返回一个强制转换为 TYPE* 的指针。返回时,p 将是指向新内存区域的指针,如果失败则为 NULL

这是一个 C 预处理器宏;p 总是被重新赋值。在处理错误时,请保存 p 的原始值以避免内存丢失。

void PyMem_Del(void *p)

PyMem_Free() 相同。

此外,提供了以下宏集,用于直接调用 Python 内存分配器,而不涉及上面列出的 C API 函数。但是,请注意,它们的使用不保留 Python 版本之间的二进制兼容性,因此在扩展模块中已弃用。

  • PyMem_MALLOC(size)

  • PyMem_NEW(type, size)

  • PyMem_REALLOC(ptr, size)

  • PyMem_RESIZE(ptr, type, size)

  • PyMem_FREE(ptr)

  • PyMem_DEL(ptr)

对象分配器

以下函数集,以 ANSI C 标准为蓝本,但指定了请求零字节时的行为,可用于从 Python 堆分配和释放内存。

备注

当通过 自定义内存分配器 部分描述的方法拦截此域中的分配函数时,不能保证这些分配器返回的内存可以成功转换为 Python 对象。

默认对象分配器 使用 pymalloc 内存分配器

警告

使用这些函数时必须存在 附加线程状态

void *PyObject_Malloc(size_t n)
作为 稳定 ABI 的一部分。

分配 n 字节,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL

如果可能,请求零字节会返回一个不同的非 NULL 指针,就像调用了 PyObject_Malloc(1) 一样。内存将不会以任何方式初始化。

void *PyObject_Calloc(size_t nelem, size_t elsize)
自 3.7 版本起成为 稳定ABI 的一部分。

分配 nelem 个元素,每个元素的字节大小为 elsize,并返回指向已分配内存的 void* 类型指针,如果请求失败则返回 NULL。内存初始化为零。

如果可能,请求零元素或零字节大小的元素会返回一个不同的非 NULL 指针,就像调用了 PyObject_Calloc(1, 1) 一样。

在 3.5 版本加入。

void *PyObject_Realloc(void *p, size_t n)
作为 稳定 ABI 的一部分。

p 指向的内存块大小调整为 n 字节。内容在旧大小和新大小的最小值范围内保持不变。

如果 pNULL,则调用等同于 PyObject_Malloc(n);否则,如果 n 等于零,则内存块大小已调整但未释放,并且返回的指针是非 NULL

除非 pNULL,否则它必须是由先前调用 PyObject_Malloc()PyObject_Realloc()PyObject_Calloc() 返回的。

如果请求失败,PyObject_Realloc() 返回 NULL,并且 p 仍然是先前内存区域的有效指针。

void PyObject_Free(void *p)
作为 稳定 ABI 的一部分。

释放 p 指向的内存块,该内存块必须是由先前调用 PyObject_Malloc()PyObject_Realloc()PyObject_Calloc() 返回的。否则,或者如果之前已调用 PyObject_Free(p),则会发生未定义行为。

如果 pNULL,则不执行任何操作。

不要直接调用此函数来释放对象的内存;请改用类型的 tp_free 槽。

不要将其用于通过 PyObject_GC_NewPyObject_GC_NewVar 分配的内存;请改用 PyObject_GC_Del()

参见

默认内存分配器

默认内存分配器

配置

名称

PyMem_RawMalloc

PyMem_Malloc

PyObject_Malloc

发布版本

"pymalloc"

malloc

pymalloc

pymalloc

调试版本

"pymalloc_debug"

malloc + 调试

pymalloc + 调试

pymalloc + 调试

发布版本,不带 pymalloc

"malloc"

malloc

malloc

malloc

调试版本,不带 pymalloc

"malloc_debug"

malloc + 调试

malloc + 调试

malloc + 调试

图例

定制内存分配器

在 3.4 版本加入。

type PyMemAllocatorEx

用于描述内存块分配器的结构体。该结构体具有以下字段:

字段

含义

void *ctx

作为第一个参数传递的用户上下文

void* malloc(void *ctx, size_t size)

分配一个内存块

void* calloc(void *ctx, size_t nelem, size_t elsize)

分配一个用零初始化的内存块

void* realloc(void *ctx, void *ptr, size_t new_size)

分配或调整内存块大小

void free(void *ctx, void *ptr)

释放内存块

3.5 版本变更: PyMemAllocator 结构体被重命名为 PyMemAllocatorEx,并添加了一个新的 calloc 字段。

type PyMemAllocatorDomain

用于标识分配器域的枚举。域:

PYMEM_DOMAIN_RAW

函数

PYMEM_DOMAIN_MEM

函数

PYMEM_DOMAIN_OBJ

函数

void PyMem_GetAllocator(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)

获取指定域的内存块分配器。

void PyMem_SetAllocator(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)

设置指定域的内存块分配器。

新分配器在请求零字节时必须返回一个不同的非 NULL 指针。

对于 PYMEM_DOMAIN_RAW 域,分配器必须是线程安全的:当调用分配器时,线程状态附加

对于其余域,分配器也必须是线程安全的:分配器可以在不共享 GIL 的不同解释器中调用。

如果新分配器不是钩子(不调用先前的分配器),则必须调用 PyMem_SetupDebugHooks() 函数以重新在新分配器之上安装调试钩子。

另请参见 PyPreConfig.allocator使用 PyPreConfig 预初始化 Python

警告

PyMem_SetAllocator() 具有以下约定:

  • 它可以在 Py_PreInitialize() 之后和 Py_InitializeFromConfig() 之前调用,以安装自定义内存分配器。除了域强加的限制(例如,原始域允许在没有 附加线程状态 的情况下调用分配器)之外,对已安装的分配器没有其他限制。有关更多信息,请参阅 分配器域部分

  • 如果在 Python 完成初始化后(在调用 Py_InitializeFromConfig() 之后)调用,则分配器 必须 包装现有分配器。不支持 将当前分配器替换为其他任意分配器。

3.12 版本变更: 所有分配器都必须是线程安全的。

void PyMem_SetupDebugHooks(void)

设置 Python 内存分配器中的调试钩子 以检测内存错误。

Python 内存分配器上的调试钩子

Python 以调试模式构建时,在 Python 预初始化 期间会调用 PyMem_SetupDebugHooks() 函数,以在 Python 内存分配器上设置调试钩子,从而检测内存错误。

PYTHONMALLOC 环境变量可用于在发布模式下编译的 Python 上安装调试钩子(例如:PYTHONMALLOC=debug)。

PyMem_SetupDebugHooks() 函数可用于在调用 PyMem_SetAllocator() 后设置调试钩子。

这些调试钩子会用特殊的、可识别的位模式填充动态分配的内存块。新分配的内存填充字节 0xCD (PYMEM_CLEANBYTE),已释放的内存填充字节 0xDD (PYMEM_DEADBYTE)。内存块被“禁用字节”包围,填充字节 0xFD (PYMEM_FORBIDDENBYTE)。这些字节字符串不太可能是有效的地址、浮点数或 ASCII 字符串。

运行时检查

发生错误时,调试钩子使用 tracemalloc 模块获取分配内存块的回溯。仅当 tracemalloc 正在跟踪 Python 内存分配且内存块已被跟踪时,才会显示回溯。

S = sizeof(size_t)。对于每个请求的 N 字节块,两端各添加 2*S 字节。内存布局如下,其中 p 表示 malloc 类似或 realloc 类似函数返回的地址(p[i:j] 表示从包含 *(p+i) 到不包含 *(p+j) 的字节切片;请注意,负索引的处理与 Python 切片不同):

p[-2*S:-S]

最初请求的字节数。这是一个 size_t,大端字节序(在内存转储中更容易读取)。

p[-S]

API 标识符(ASCII 字符)

p[-S+1:0]

PYMEM_FORBIDDENBYTE 的副本。用于捕获欠写入和欠读取。

p[0:N]

请求的内存,填充有 PYMEM_CLEANBYTE 的副本,用于捕获对未初始化内存的引用。当调用 realloc 类似函数请求更大的内存块时,新的多余字节也填充有 PYMEM_CLEANBYTE。当调用 free 类似函数时,这些字节被 PYMEM_DEADBYTE 覆盖,以捕获对已释放内存的引用。当调用 realloc 类似函数请求更小的内存块时,多余的旧字节也填充有 PYMEM_DEADBYTE

p[N:N+S]

PYMEM_FORBIDDENBYTE 的副本。用于捕获过写入和过读取。

p[N+S:N+2*S]

仅在定义 PYMEM_DEBUG_SERIALNO 宏时使用(默认未定义)。

一个序列号,每次调用 malloc 类似或 realloc 类似函数时递增 1。大端 size_t。如果稍后检测到“坏内存”,序列号提供了一种在下一次运行时设置断点的好方法,以捕获此块被传递出的瞬间。obmalloc.c 中的静态函数 bumpserialno() 是序列号唯一递增的地方,它的存在就是为了让您可以轻松设置这样的断点。

realloc-like 或 free-like 函数首先检查两端的 PYMEM_FORBIDDENBYTE 字节是否完好无损。如果它们已被更改,诊断输出将写入 stderr,程序将通过 Py_FatalError() 中止。另一个主要的故障模式是当程序读取其中一个特殊位模式并尝试将其用作地址时引发内存错误。如果您此时进入调试器并查看对象,您很可能会发现它完全填充了 PYMEM_DEADBYTE(表示正在使用已释放的内存)或 PYMEM_CLEANBYTE(表示正在使用未初始化的内存)。

3.6 版本变更: PyMem_SetupDebugHooks() 函数现在也适用于发布模式下编译的 Python。发生错误时,调试钩子现在使用 tracemalloc 获取内存块分配时的回溯。调试钩子现在还检查当调用 PYMEM_DOMAIN_OBJPYMEM_DOMAIN_MEM 域的函数时是否存在 附加线程状态

3.8 版本变更: 字节模式 0xCB (PYMEM_CLEANBYTE)、0xDB (PYMEM_DEADBYTE) 和 0xFB (PYMEM_FORBIDDENBYTE) 已被 0xCD0xDD0xFD 替换,以使用与 Windows CRT 调试 malloc()free() 相同的值。

pymalloc 分配器

Python 有一个 pymalloc 分配器,它针对具有短生命周期的小对象(小于或等于 512 字节)进行了优化。它使用固定大小的内存映射,称为“arena”,在 32 位平台上为 256 KiB,在 64 位平台上为 1 MiB。对于大于 512 字节的分配,它会回退到 PyMem_RawMalloc()PyMem_RawRealloc()

pymallocPYMEM_DOMAIN_MEM(例如:PyMem_Malloc())和 PYMEM_DOMAIN_OBJ(例如:PyObject_Malloc())域的 默认分配器

arena 分配器使用以下函数:

  • Windows 上的 VirtualAlloc()VirtualFree()

  • 如果可用,则为 mmap()munmap()

  • 否则为 malloc()free()

如果 Python 使用 --without-pymalloc 选项配置,则此分配器将被禁用。也可以使用 PYTHONMALLOC 环境变量在运行时禁用它(例如:PYTHONMALLOC=malloc)。

通常,在使用 AddressSanitizer (--with-address-sanitizer) 构建 Python 时禁用 pymalloc 分配器是很有意义的,这有助于发现 C 代码中的低级错误。

定制 pymalloc Arena 分配器

在 3.4 版本加入。

type PyObjectArenaAllocator

用于描述 arena 分配器的结构。该结构有三个字段:

字段

含义

void *ctx

作为第一个参数传递的用户上下文

void* alloc(void *ctx, size_t size)

分配一个大小为 size 字节的 arena

void free(void *ctx, void *ptr, size_t size)

释放一个 arena

void PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)

获取 arena 分配器。

void PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)

设置 arena 分配器。

mimalloc 分配器

在 3.13 版本加入。

当底层平台支持时,Python 支持 mimalloc 分配器。mimalloc “是一个具有出色性能特征的通用分配器。最初由 Daan Leijen 为 Koka 和 Lean 语言的运行时系统开发。”

tracemalloc C API

在 3.7 版本加入。

int PyTraceMalloc_Track(unsigned int domain, uintptr_t ptr, size_t size)

tracemalloc 模块中跟踪已分配的内存块。

成功返回 0,错误返回 -1(分配内存以存储跟踪失败)。如果 tracemalloc 被禁用,则返回 -2

如果内存块已被跟踪,则更新现有跟踪。

int PyTraceMalloc_Untrack(unsigned int domain, uintptr_t ptr)

tracemalloc 模块中取消跟踪已分配的内存块。如果该块未被跟踪,则不执行任何操作。

如果 tracemalloc 被禁用,则返回 -2,否则返回 0

示例

以下是 概述 部分的示例,通过使用第一个函数集从 Python 堆分配 I/O 缓冲区进行重写:

PyObject *res;
char *buf = (char *) PyMem_Malloc(BUFSIZ); /* for I/O */

if (buf == NULL)
    return PyErr_NoMemory();
/* ...Do some I/O operation involving buf... */
res = PyBytes_FromString(buf);
PyMem_Free(buf); /* allocated with PyMem_Malloc */
return res;

使用面向类型的函数集的相同代码:

PyObject *res;
char *buf = PyMem_New(char, BUFSIZ); /* for I/O */

if (buf == NULL)
    return PyErr_NoMemory();
/* ...Do some I/O operation involving buf... */
res = PyBytes_FromString(buf);
PyMem_Free(buf); /* allocated with PyMem_New */
return res;

请注意,在上面两个示例中,缓冲区始终通过属于同一组的函数进行操作。实际上,对于给定的内存块,需要使用相同的内存 API 系列,以最大限度地减少混合不同分配器的风险。以下代码序列包含两个错误,其中一个被标记为 致命,因为它混合了两个在不同堆上操作的不同分配器。

char *buf1 = PyMem_New(char, BUFSIZ);
char *buf2 = (char *) malloc(BUFSIZ);
char *buf3 = (char *) PyMem_Malloc(BUFSIZ);
...
PyMem_Del(buf3);  /* Wrong -- should be PyMem_Free() */
free(buf2);       /* Right -- allocated via malloc() */
free(buf1);       /* Fatal -- should be PyMem_Free()  */

除了旨在处理来自 Python 堆的原始内存块的函数之外,Python 中的对象使用 PyObject_NewPyObject_NewVarPyObject_Free() 进行分配和释放。

这些将在下一章关于在 C 中定义和实现新对象类型中解释。