系列导读:本系列面向有一定 Python 基础的开发者,深入讲解 Python 高级特性与工程实践。建议按顺序阅读,每篇包含完整知识体系、底层原理剖析与实战项目。
前置知识:第 10-11 篇(面向对象)、第 16 篇(上下文管理器)、第 17 篇(多线程与 GIL)
引言:为什么你需要理解 Python 的内存管理?
大多数 Python 开发者从未手动调用过 malloc 或 free,这得益于 Python 的自动内存管理系统。然而,“自动\”不等于\”完美”——内存泄漏、GC 暂停、循环引用导致的对象无法回收等问题,在生产环境中屡见不鲜。
更深层的原因是:理解内存管理是理解 Python 一切行为的基础。为什么 a is b 有时为 True 有时为 False?为什么 __del__ 不总是被调用?为什么 GIL 的存在与引用计数息息相关?这些问题的答案都藏在内存管理的底层机制中。
本文将从 CPython 源码级别的对象模型出发,逐层剖析引用计数、分代回收、弱引用机制,并给出生产级的内存诊断与优化方案。
一、CPython 对象模型:一切皆对象的内存真相
1.1 PyObject:所有对象的公共头部
在 CPython 的 C 实现中,每个 Python 对象——无论整数、字符串、列表还是自定义类实例——都以一个 PyObject 结构体开头:
// [Objects/object.h] CPython 源码
typedef struct _object {
Py_ssize_t ob_refcnt; // 引用计数器
PyTypeObject *ob_type; // 类型对象指针
} PyObject;
这意味着每个 Python 对象至少占用 16 字节(64 位系统下:8 字节引用计数 + 8 字节类型指针),这还不包括对象自身的数据。对于数值型对象,CPython 还需要一个额外的\”值\”字段:
typedef struct {
PyObject ob_base; // 16 bytes:继承自 PyObject
Py_ssize_t ob_size; // 8 bytes:表示数值的\”位数\”
digit ob_digit[1]; // 变长:实际存储数值
} PyLongObject;
这就是为什么 sys.getsizeof(0) 返回 28 字节(16 + 8 + 4,含对齐填充),而 sys.getsizeof(2**30) 返回 32 字节——更大的整数需要更多的 digit 存储。
1.2 类型指针的深远影响
ob_type 指针指向对象的类型对象(PyTypeObject),它本身也是一个 PyObject。这个设计决定了 Python 中\”类型也是对象\”的核心语义:
- type(42) 返回 <class \’int\’>,实际上是通过 ob_type 指针找到 int 类型对象
- isinstance(x, int) 实际上是检查 x.ob_type 是否指向 int 或其子类
- 动态修改类(猴子补丁)之所以可行,正是因为类型信息存储在指针中而非编译时确定
关键洞察:ob_type 指针是 Python 多态和动态类型的硬件级实现。方法调用 obj.method() 时,解释器通过 ob_type 查找方法的实际地址——这就是 Python 方法调用的开销比 C 函数调用大的根本原因之一。
1.3 容器对象的内部结构
列表和字典是 Python 中最常用的容器,它们的内存结构值得深入理解:
列表(PyListObject):
typedef struct {
PyObject ob_base; // 16 bytes
Py_ssize_t ob_size; // 8 bytes:当前元素数量
PyObject **ob_item; // 8 bytes:指向元素指针数组的指针
Py_ssize_t allocated; // 8 bytes:已分配的槽位数(≥ ob_size)
} PyListObject;
列表的 ob_item 指向一个连续的 PyObject* 数组。每个元素是一个 8 字节的指针,指向实际的 Python 对象。这意味着:
- 空列表占用 56 字节(16 + 8 + 8 + 8 + 对齐填充)
- 列表本身只存储指针,不存储对象数据——[1, 2, 3] 中,列表持有三个指针,分别指向三个独立的 int 对象
- 列表的 allocated 通常大于 ob_size,这是\”过度分配\”策略,用于摊销 append 的扩容成本
过度分配的 growth pattern:当列表空间不足时,CPython 按照公式 new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize 计算新容量。这意味着列表的容量增长模式大致为:0 → 4 → 8 → 16 → 25 → 35 → 46 → 58 → …,而非简单的 2 倍增长。
字典(PyDictObject):Python 3.6+ 使用紧凑字典(compact dict),将键、值、哈希值存储在三个并行数组中,内存效率比 Python 3.5 提升了约 20-25%。
1.4 验证对象内存开销
import sys
# 基础对象开销
print(f\”空对象: {
sys.getsizeof(object())} bytes\”) # 16
print(f\”整数 0: {
sys.getsizeof(0)} bytes\”) # 28
print(f\”整数 2**30: {
sys.getsizeof(2**30)} bytes\”) # 32
print(f\”空列表: {
sys.getsizeof([])} bytes\”) # 56
print(f\”空字典: {sys.getsizeof({})} bytes\”) # 72
print(f\”空元组: {
sys.getsizeof(())} bytes\”) # 40
print(f\”空集合: {
sys.getsizeof(set())} bytes\”) # 232
print(f\”空字符串: {
sys.getsizeof(\’\’)} bytes\”) # 49
这些数字揭示了 Python 内存管理的第一个重要事实:Python 对象有显著的固定开销。一个空字典占 72 字节,一个空集合占 232 字节——在创建大量小容器时,这个开销会迅速累积。
二、内存分配器的四层架构
Python 的内存分配并非直接调用操作系统的 malloc,而是通过四层架构逐层抽象:
┌──────────────────────────────────────────────────┐
│ 第 3 层:对象特定分配器




