写 Python 的人很少关心一个对象创建之后内存是怎么分配的,销毁之后内存又是怎么回收的——直到程序莫名其妙吃掉几 GB 内存,或者某个对象该释放却迟迟不释放。理解 CPython 的内存管理,不是底层考古,而是排查问题的实用工具。
引用计数:对象存活的第一道防线
CPython 中每个对象头部都有一个 ob_refcnt 字段,记录有多少个引用指向它。引用增加时计数 +1,引用离开作用域或被显式删除时计数 -1。计数归零,对象立刻被销毁,内存释放——没有延迟,没有等待。
这意味着 Python 的绝大多数对象释放是确定性的:你 del x,如果这是最后一个引用,对象马上消失。
import sys
s = "hello"
print(sys.getrefcount(s)) # 通常是 2:变量 s + getrefcount 的临时引用
t = s # 新引用,计数 +1
print(sys.getrefcount(s)) # 3
del t # 引用消失,计数 -1
print(sys.getrefcount(s)) # 2
del s # 计数归零,字符串对象被释放
注意 sys.getrefcount() 本身会临时创建一个引用,所以你看到的值总是比"真实"多 1。这是初学者常犯的误读。
引用计数的致命缺陷:循环引用。两个对象互相引用对方,即使外部已经没人持有它们,彼此的计数也不会归零。
import sys
class Node:
def __init__(self, name):
self.name = name
self.partner = None
a = Node("A")
b = Node("B")
a.partner = b
b.partner = a
print(sys.getrefcount(a)) # 2:变量 a + b.partner
del a
del b
# A 和 B 互相引用,引用计数各为 1,永远不会归零——内存泄漏
这就是垃圾回收器存在的理由。
垂暮回收器:专治循环引用
CPython 的垃圾回收器(gc)只处理容器类型的循环引用——list、dict、set、自定义类实例等。不可变类型(int、str、tuple)不会参与 gc 循环检测。
gc 的策略是分代回收:新对象放在第 0 代,存活过一次回收的对象升到第 1 代,再存活升到第 2 代。越老的代,回收频率越低。这是基于"大部分对象都很短命"的经验假设。
import gc
# 查看当前各代阈值
print(gc.get_threshold()) # 默认 (700, 10, 5):第0代累计700次分配触发回收
# 手动触发回收
collected = gc.collect()
print(f"回收了 {collected} 个对象")
# 查看当前 tracked 对象数量
print(len(gc.get_objects())) # 所有被 gc 追踪的容器对象
前面那个 Node 循环引用的例子,靠 gc 就能解决:
import gc
class Node:
def __init__(self, name):
self.name = name
self.partner = None
a = Node("A")
b = Node("B")
a.partner = b
b.partner = a
del a
del b
# 此时 A 和 B 仍在内存中,循环引用
collected = gc.collect()
print(f"gc 回收了 {collected} 个对象") # 通常会回收 2 个(A 和 B)
但 gc 也有盲区:如果循环引用链中某个对象定义了 __del__ 方法,Python 3.4 之前 gc 无法判断回收顺序,干脆不回收。PEP 442 改进了这一点,现代 CPython 可以处理带 __del__ 的循环引用,但最好避免在可能参与循环的对象上定义 __del__。
内存分配器:从操作系统到对象的三层结构
引用计数和 gc 决定对象何时死亡,但对象诞生时内存从哪来?CPython 有三层分配策略:
- 大块内存(≥ 512 字节):直接调用系统
malloc/free,不走 Python 自有分配器。 - 小块内存(< 512 字节):由 pymalloc 管理。pymalloc 把内存组织为 Arena → Pool → Block 三级结构。Arena 是 256 KB 的大块,从操作系统申请;Pool 是 4 KB,属于某个 Arena;Block 是实际分配给对象的最小单元,大小按 8 字节对齐。同大小的对象共享同一个 Pool,减少碎片。
- Python 对象层:在 pymalloc 之上,每种类型有自己的分配策略。比如小整数(-5 到 256)是预创建的常量,永远不会被释放;短字符串也有类似的 intern 机制。
# 小整数缓存:这些整数对象是预分配的,id 永远不变
a = 256
b = 256
print(a is b) # True,同一个对象
c = 257
d = 257
print(c is d) # False(通常),超出了小整数缓存范围
了解这个分层结构有助于理解一个现象:Python 进程的 RSS(驻留内存)往往只增不减。Arena 被 pymalloc 拿走后,即使里面的对象全被释放,Arena 也不会立刻还给操作系统——它被标记为空闲,等待下次复用。所以"Python 进程内存不下降"不等于泄漏,很可能只是 pymalloc 在持有空闲 Arena。
实战排查:找到吃内存的元凶
理论讲完了,真正有用的是排查工具。以下是三个可以直接拿来用的方法。
1. tracemalloc:追踪分配来源
tracemalloc 是 Python 3.4+ 内置的内存追踪器,能记录每一块内存是在哪一行代码分配的。
import tracemalloc
tracemalloc.start()
# —— 你的业务代码 ——
big_list = [str(i) * 1000 for i in range(100000)]
# —— 业务代码结束 ——
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
输出类似:
/tmp/test.py:5: size=99.5 MB, count=100000, average=1015 B
直接告诉你第 5 行分配了 99.5 MB,一目了然。
2. objgraph:可视化引用关系
当怀疑有循环引用或意外持有,objgraph 能画出引用链。
pip install objgraph
import objgraph
class Node:
def __init__(self, name):
self.name = name
self.partner = None
a = Node("A")
b = Node("B")
a.partner = b
b.partner = a
# 查看 Node 实例的引用链
objgraph.show_backrefs([a], max_depth=5, filename="refs.png")
会生成一张 PNG 图,清晰展示谁引用了谁,循环引用一目了然。
3. gc + 弱引用:打破循环
如果两个对象确实需要互相知道对方,但又不想形成强引用循环,用 weakref:
import weakref
class Node:
def __init__(self, name):
self.name = name
self._partner_ref = None
def set_partner(self, other):
# 用弱引用持有对方,不增加引用计数
self._partner_ref = weakref.ref(other)
@property
def partner(self):
if self._partner_ref is not None:
return self._partner_ref() # 返回真实对象或 None(如果对方已死)
return None
a = Node("A")
b = Node("B")
a.set_partner(b)
b.set_partner(a)
print(a.partner.name) # "B",正常访问
del b
print(a.partner) # None,b 已被回收,弱引用自动失效
弱引用不增加引用计数,对方死亡后 ref() 返回 None。这是解决循环引用最干净的方式,比依赖 gc 更可控。
写代码时的几个决策点
| 场景 | 建议 |
|---|---|
| 对象之间需要双向关联 | 用 weakref 代替强引用 |
| 需要确认对象是否被意外持有 | objgraph.show_backrefs() |
| 进程内存持续增长 | 先用 tracemalloc 定位分配热点,再判断是泄漏还是 pymalloc 持有空闲 Arena |
自定义类定义了 __del__ |
尽量避免,尤其对象可能参与循环引用时;用上下文管理器(__enter__/__exit__)替代 |
| 调试引用计数 | sys.getrefcount(),记住结果比真实值多 1 |
Python 的内存管理不是黑箱——引用计数是确定性回收,gc 是兜底机制,pymalloc 是分配引擎。三者配合决定了对象的生与死。理解它们,不是为了写更"底层"的代码,而是为了在内存出问题时不再盲目猜测。