Python 对象的生与死:CPython 内存管理机制详解

2026-05-08 26 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

写 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 有三层分配策略:

  1. 大块内存(≥ 512 字节):直接调用系统 malloc/free,不走 Python 自有分配器。
  2. 小块内存(< 512 字节):由 pymalloc 管理。pymalloc 把内存组织为 Arena → Pool → Block 三级结构。Arena 是 256 KB 的大块,从操作系统申请;Pool 是 4 KB,属于某个 Arena;Block 是实际分配给对象的最小单元,大小按 8 字节对齐。同大小的对象共享同一个 Pool,减少碎片。
  3. 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 是分配引擎。三者配合决定了对象的生与死。理解它们,不是为了写更"底层"的代码,而是为了在内存出问题时不再盲目猜测。


相关推荐