PyObject 与引用计数:Python 的内存真相 (Python Memory Model)
章节概述
你写了 a = [1, 2, 3],Python 到底做了什么?本章从 CPython 源码出发,揭示 Python 内存管理的核心机制:一切皆 PyObject、引用计数机制、垃圾回收的循环检测算法。我们将 Python 的动态内存模型与 C 的静态 malloc/free 模型逐项对比,让你彻底理解”为什么 Python 很慢”以及”为什么 Python 很安全”。
核心理念:Python 的内存模型不是魔法,而是一层构建在 C
malloc/free之上的引用计数系统。理解了 PyObject,你就理解了 Python 的一切。
第一节:一切皆 PyObject
在 CPython 的世界里,一切数据都是 PyObject。无论是整数 42、字符串 "hello"、列表 [1,2,3],还是函数、类、模块——它们在 C 层面都用一个结构体表示。
来看看 CPython 源码中 PyObject 的定义(简化自 Include/object.h):
// CPython 源码: Include/object.h
// 这是每个 Python 对象都有的"头部"
typedef struct _object {
Py_ssize_t ob_refcnt; // 引用计数 — 这个对象被引用了多少次
PyTypeObject *ob_type; // 指向类型对象的指针 — 这个对象是什么类型
} PyObject;每一个 Python 对象在 C 层面至少包含这两个字段:
| 字段 | 含义 | C 类比 |
|---|---|---|
ob_refcnt | 引用计数:GC 用它判断对象能否被释放 | C 中不存在,C 由程序员手动决定 free 时机 |
ob_type | 类型指针:指向描述该对象类型的元对象 | 类似于 void* 配合手动记录的类型标签 |
再看整数对象的实际定义(Include/longintrepr.h):
typedef struct {
PyObject ob_base; // 继承"头部" — ob_refcnt + ob_type
Py_ssize_t ob_size; // 数字的"位数"
uint32_t ob_digit[1]; // 实际数字数据(柔性数组)
} PyLongObject;关键对比:C 语言的
int就是 4 字节的裸值,存储在寄存器或栈上。Python 的int是一个在堆上分配的PyLongObject结构体,包含类型信息、引用计数和实际数值。这就是 Python 比 C 慢几十倍的底层原因之一:x = 1 + 2不是一条 CPU 指令,而是数十次函数调用和堆分配。
用 Python 验证:
python -c "
import sys
a = 42
b = [1, 2, 3]
print(f'int 大小: {sys.getsizeof(a)} 字节') # 28 字节!
print(f'list 大小: {sys.getsizeof(b)} 字节') # 远超数据本身的体积
print(f'a 的类型: {type(a)} -> {type(a).__mro__}')
"输出:
int 大小: 28 字节
list 大小: 88 字节
a 的类型: <class 'int'> -> (<class 'int'>, <class 'object'>)
一个 C 的 int 只有 4 字节,Python 的 int 却要 28 字节——额外的 24 字节就是 PyObject 头部、引用计数槽位和动态精度管理的代价。
第二节:引用计数机制
2.1 引用计数的规则
CPython 使用引用计数作为主要的内存管理策略。规则很简单:
| 操作 | 引用计数变化 |
|---|---|
a = SomeObject() | ob_refcnt = 1 |
b = a | ob_refcnt += 1 |
del b | ob_refcnt -= 1 |
a 离开作用域 | ob_refcnt -= 1 |
ob_refcnt == 0 | 立即调用 __del__,然后 free 内存 |
python -c "
import sys
a = [] # 创建空列表,refcnt = 1
print(sys.getrefcount(a)) # getrefcount 本身会增加 1 个临时引用!
b = a # refcnt = 2
print(sys.getrefcount(a))
del b # refcnt = 1 (b 的引用消失)
print(sys.getrefcount(a) - 1)
"输出:
2 # getrefcount 的参数引用 + 变量 a = 2
3 # getrefcount + a + b = 3
1 # 实际 refcnt 是 2,减去 getrefcount 的临时引用 = 1
重要陷阱:
sys.getrefcount()的结果总是比”实际”引用数大 1,因为它自己的参数就是一个临时引用。
2.2 引用计数 vs C 手动内存管理
// C 语言:手动 malloc/free — 完全由程序员负责
#include <stdlib.h>
#include <stdio.h>
typedef struct {
int *data;
int size;
} IntArray;
IntArray* create_array(int size) {
IntArray *arr = malloc(sizeof(IntArray));
arr->data = malloc(sizeof(int) * size);
arr->size = size;
return arr;
}
void destroy_array(IntArray *arr) {
free(arr->data);
free(arr);
}
// 使用 — 危险!忘记 free 导致泄漏,过早 free 导致悬垂指针
int main() {
IntArray *a = create_array(100);
IntArray *b = a; // 两个指针指向同一块内存
destroy_array(a); // free 后 b 是悬垂指针!
// b->data[0] = 42; // 段错误或未定义行为!
return 0;
}# Python:引用计数自动管理
a = [0] * 100
b = a # refcnt += 1,两个名字指向同一对象
del a # refcnt -= 1,[] 的 refcnt 仍 >= 1,不会被释放
print(b[0]) # 安全!b 仍然持有引用
del b # refcnt -= 1 → 0,解释器自动释放内存关键差异:C 中
b = a只是复制指针值,程序员需要跟踪”所有权”。Python 中b = a是增加引用计数,无论谁先释放都不会影响另一个。
第三节:循环引用与 GC 模块
引用计数有一个致命弱点:无法处理循环引用。
python -c "
import sys
a = []
b = []
a.append(b) # a[0] 引用 b → b 的 refcnt +1
b.append(a) # b[0] 引用 a → a 的 refcnt +1
print('a refcnt:', sys.getrefcount(a) - 1) # 2 (变量 a + b[0])
print('b refcnt:', sys.getrefcount(b) - 1) # 2 (变量 b + a[0])
del a
del b
# 此时 a 和 b 变量的引用消失,但 a[0] 和 b[0] 互相引用
# 两个列表的 refcnt 都是 1(互相持有)→ 永远不会归零 → 内存泄漏!
"输出:
a refcnt: 2
b refcnt: 2
# del a, del b 后,两个列表互相引用,但已经没有任何外部引用 → 垃圾!
3.1 GC 的循环检测
CPython 除了引用计数,还有一个分代垃圾回收器专门处理循环引用:
import gc
import sys
# 查看 GC 配置
print(gc.get_threshold()) # (700, 10, 10) — 三代阈值
# 手动触发 GC
gc.collect() # 强制运行垃圾回收
# 禁用自动 GC(仅保留引用计数)
gc.disable()
# 现在循环引用会导致内存泄漏!
# 重新启用
gc.enable()GC 将对象分为三代:
| 代 | 说明 | 回收频率 |
|---|---|---|
| gen0 | 新创建的对象 | 每次阈值达到都回收 |
| gen1 | 经历一次回收后存活的对象 | gen0 回收 10 次后回收一次 |
| gen2 | 经历两次回收后存活的对象 | gen1 回收 10 次后回收一次 |
python -c "
import gc
print('GC 启用状态:', gc.isenabled())
print('三代阈值:', gc.get_threshold())
print('可回收对象数:', len(gc.get_objects()))
print('统计:', gc.get_stats())
"3.2 __del__ vs C 的析构/free
// C 语言:手动释放
typedef struct {
FILE *fp;
char *buffer;
} FileWrapper;
void file_wrapper_free(FileWrapper *fw) {
if (fw->fp) fclose(fw->fp); // 必须手动销毁每个资源
free(fw->buffer);
free(fw);
}class FileWrapper:
def __init__(self, path):
self.fp = open(path, 'w')
self.buffer = []
def __del__(self):
"""析构函数 — 在 ob_refcnt 归零时由解释器调用"""
self.fp.close()
print("资源已释放")
# 使用 with 语句更可靠(不依赖引用计数归零的时机)
with open('test.txt', 'w') as f:
f.write('hello')
# with 块结束,文件立即关闭——类似 C 的 RAIIC vs Python 析构对比:C 的
free()是确定性的(调用即释放);Python 的__del__调用时机不确定(取决于 GC),甚至永远不会被调用(如果发生循环引用且 GC 被禁用)。Python 中推荐使用with上下文管理器而不是__del__。
第四节:Python 内存布局 vs C 内存布局
4.1 C 数组:连续内存块
// C 数组:所有元素在内存中紧密排列
int arr[5] = {10, 20, 30, 40, 50};
// 内存布局:|10|20|30|40|50| ← 连续 20 字节
// arr[3] 的地址 = arr + 3 * sizeof(int) = arr + 12 字节
// CPU 缓存友好,向量化友好4.2 Python 列表:PyObject* 指针数组
# Python 列表:存储的是 PyObject* 指针,不是值本身
arr = [10, 20, 30, 40, 50]Python 列表在 C 层面的结构(简化):
typedef struct {
PyObject ob_base; // ob_refcnt + ob_type
Py_ssize_t ob_size; // 列表长度
PyObject **ob_item; // 指向指针数组的指针!
Py_ssize_t allocated; // 预分配容量
} PyListObject;内存布局对比图:
C 数组:
[10][20][30][40][50]
↑ ↑
arr arr+4
Python 列表:
ob_item → [*][*][*][*][*]
↓ ↓ ↓ ↓ ↓
PyLongObject PyLongObject ...
(28 字节) (28 字节)
每个元素是一个独立的堆对象,通过指针间接访问!
性能启示:遍历 Python 列表时,CPU 不仅要读取指针数组,还要跳转到各个 PyLongObject 的地址。每次访问都有指针追逐开销,严重破坏 CPU 缓存。C 数组遍历是可以被编译器自动向量化(SIMD)的连续内存访问。这是 Python 数值计算慢的另一个根本原因——也是 NumPy 存在的理由。
4.3 小整数缓存
CPython 有一个有趣的小整数缓存机制:
python -c "
a = 256
b = 256
print(a is b) # True — 小整数被缓存,a 和 b 指向同一个对象
c = 257
d = 257
print(c is d) # False — 超出缓存范围,各自创建新对象
# 缓存范围:-5 到 256(包含)
# 这些整数在解释器启动时预分配,永不释放
"输出:
True
False
这是 CPython 的实现细节(
NSMALLPOSINTS和NSMALLNEGINTS宏),不要在生产代码中依赖is比较整数。
第五节:弱引用
有时我们需要引用一个对象,但不希望阻止它被垃圾回收——这就是弱引用。
import weakref
import sys
class BigObject:
def __del__(self):
print("BigObject 被回收了")
obj = BigObject()
ref = weakref.ref(obj) # 弱引用 — 不增加引用计数!
print(ref()) # <__main__.BigObject object at 0x...>
print(sys.getrefcount(obj)) # 2 (变量 + getrefcount),不含弱引用
del obj # refcnt → 0,对象被回收
print(ref()) # None — 对象已不存在输出:
2
BigObject 被回收了
None
C 对比:C 中没有弱引用的概念。如果你需要”观察但不拥有”的语义,只能手动标记指针为”非所有者”并用注释或约定来管理——非常容易出错。
弱引用的典型用途:
- 缓存系统中,缓存对象的引用不阻止其被回收
- 观察者模式中,观察者不阻止被观察对象销毁
- 循环引用的解决方案之一
import weakref
class Cache:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get(self, key, factory):
obj = self._cache.get(key)
if obj is None:
obj = factory()
self._cache[key] = obj
return obj
# 如果外部不再引用对象,Cache 中的条目自动消失练习
以下题目用于验证本章所学内容:
| 题号 | 题目 | 链接 | 涉及知识点 |
|---|