Python 内存回收机制详解

引用计数 · 分代垃圾回收 · 小对象池 —— 这三块拼图共同构成了 Python 高效的内存管理

#1
引用计数 · Reference Counting
#2
分代回收 · Generational GC
#3
小对象池 · pymalloc

Python 的内存管理像一个精密的管家系统——它会自动跟踪每个对象被引用了多少次,在没人用的时候立即回收;对于循环引用这种引用计数搞不定的情况,它还有一套独立的分代垃圾回收器;而对大量小块对象的频繁分配,它直接向操作系统批发内存,再在内部做精细管理。下面逐一拆解这三块拼图。

一、引用计数 Reference Counting

这是 Python 内存回收的第一道防线,也是最主要的机制。每个 Python 对象在内存中都带有一个 ob_refcnt 字段,记录当前有多少个引用指向它。当引用计数降到 0 时,对象被立即销毁,内存归还。

核心规则

交互演示:引用计数变化

点击按钮模拟 Python 中对一个列表对象的引用操作,观察引用计数的变化。

2
当前引用计数
变量引用: a, b
对象存活中

查看引用计数

import sys
a = []
print(sys.getrefcount(a))  # 输出: 2

# getrefcount 自身会临时+1
# 所以通常比实际多 1

引用计数的局限

循环引用是引用计数最大的盲区——两个对象互相引用时,即使外部已无法访问,它们的引用计数也永远不会归零,导致内存泄漏
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a  # 循环引用!refcnt 永不为 0

二、分代垃圾回收 Generational GC

为了解决循环引用的问题,Python 引入了分代垃圾回收器(Generational Garbage Collector)。它的核心思想来自一个观察:大多数对象生命周期很短,少数对象存活很久

第 0 代 (Gen 0) 年轻对象 · 扫描频率最高 新创建的 List 短命临时对象 循环引用候选 第 1 代 (Gen 1) 存活过一次 GC 的对象 函数返回值 模块级变量 第 2 代 (Gen 2) 长期存活 · 扫描最少 全局常量 缓存对象 晋升 晋升 阈值: ~700 allocations 阈值: ~10 GC 周期 阈值: ~10 GC 周期
三个代的阈值可以通过 gc.get_threshold() 查看和 gc.set_threshold() 调整。默认是 (700, 10, 10) —— 即第 0 代每新增约 700 个对象触发一次 GC,第 0 代每 10 次 GC 触发一次第 1 代 GC,以此类推。

GC 如何检测循环引用?

Python 的 GC 运行在 gc 模块中,它的检测算法分三步:

  1. 1
    找到所有「容器对象」

    GC 只关心可能产生循环引用的容器:list、dict、set、tuple、自定义类的实例等。int、str 等不可变基本类型不会被循环引用困住,直接跳过。

  2. 2
    复制引用计数 → 扣除内部引用

    GC 把每个容器对象的 ob_refcnt 复制一份,然后遍历每个容器的子对象,将子对象的复制版本引用计数减 1。这一步的目的是「消除内部循环的引用」。

  3. 3
    引用计数 = 0 的就是垃圾

    扣除后引用计数仍大于 0 的说明有外部引用,保留。降为 0 的说明所有引用都来自这个循环内部——这些就是不可达的循环垃圾,全部回收。

动手试试:触发 GC

import gc
import sys

# 查看当前分代统计
print(gc.get_count())  # (obj_in_gen0, obj_in_gen1, obj_in_gen2)

# 查看阈值
print(gc.get_threshold())  # 默认 (700, 10, 10)

# 手动触发垃圾回收
gc.collect()  # 完整回收(所有代)
gc.collect(generation=0)  # 只回收第 0 代

# 查看回收了多少对象
print(gc.collect())  # 返回值:回收的对象数

三、小对象池 pymalloc

Python 程序运行时会产生海量的小对象——整数、浮点数、短字符串、小列表等。如果每次都向操作系统申请和归还内存,开销巨大。Python 的做法是:向操作系统批发大块内存,然后在内部做精细化的零售管理

操作系统 · Arena 256 KB 大块内存 · 按需向 OS 申请 一次申请,多次分配 Pool · 内存池 每个 Pool 4 KB · 所有 slot 大小相同 · 来自同一个 Arena slot 8B slot 16B slot 24B slot 32B slot 48B slot 64B ... 继续到 512B 大类 (共约 64 个 size class) ≤ 512B > 512B → C malloc

核心概念速览

层级大小职责
Arena256 KB从操作系统申请的大块连续内存,一个 arena 包含多个 pool
Pool4 KB管理同一种 size class 的对象,每个 pool 只服务一种大小
Block / Slot8B ~ 512B最小的分配单元,对应一个 Python 对象
关键分界线:512 字节。不超过 512 字节的对象走 pymalloc(Python 自己的内存分配器),超过 512 字节的大对象直接走 C 标准库的 malloc。这意味着 Python 里的 int、float、小 list、小 dict、小 tuple 等绝大多数对象都享受 pymalloc 的高效管理。

为什么 pymalloc 更快?

  1. 1
    减少系统调用

    不是每次 new 一个对象都向 OS 要内存,而是预先批发一大块 arena,内部自己切分。

  2. 2
    内存复用

    对象被释放后,slot 回到空闲链表。下次分配同样大小的对象时直接复用,无需重新申请。

  3. 3
    无碎片化

    每个 pool 只管理一种大小的 slot,不存在「释放了一个 16B 对象留下 16B 空隙,但下一个要分配 24B 放不下」的问题。

  4. 4
    自由链表

    已释放的 slot 通过一个单链表连接,O(1) 取用。分配时从链表头拿,释放时插入链表头。

四、全景:三机制如何协作

这三个机制不是在比赛,而是在分工协作。理解它们的协作关系,才算真正理解了 Python 内存回收。

创建 Python 对象
pymalloc 从 pool 分配
引用计数 = 1
slot 标记为已用
引用计数 → 0
slot 回到空闲链表
下次分配直接复用
循环引用
GC 分代回收介入
回收不可达对象

正常路径:引用计数搞定一切 · 兜底路径:GC 处理循环引用 · 底层加速:pymalloc 减少系统调用

场景 A:正常短命对象

def process():
    data = [1, 2, 3]
    return sum(data)

# data 在函数返回后引用计数归零
# 内存立即被引用计数机制回收
# pymalloc 把 slot 放回空闲链表

场景 B:循环引用

class Node:
    def __init__(self):
        self.next = None

a = Node()
b = Node()
a.next = b
b.next = a  # 循环引用!

# 引用计数无法处理
# GC 在 Gen 0 扫描时检测到
# 确认不可达后释放

五、与其他语言的对比

语言 内存管理方式 特点
Python 引用计数 + 分代 GC 确定性回收(引用计数归零即释放),GC 仅处理循环引用
Java / Go 纯 Tracing GC 没有引用计数,全靠 GC 扫描。回收时机不确定,但处理循环引用很自然
C / C++ 手动管理 malloc/free 或 new/delete,程序员全权负责,性能最高但容易出错
Rust 所有权 + 借用检查 编译期确定生命周期,无 GC 无引用计数,零运行时开销
Swift ARC (自动引用计数) 类似 Python 的引用计数,但编译器自动插入 retain/release,且用 weak 打破循环
Python 的引用计数有一个特别的好处:确定性。你知道一个对象在引用计数归零的瞬间就被释放了,比如 file.close()del f 之后立即发生。在纯 GC 语言中,你只能「建议」GC 运行,无法精确控制时机。

六、实用调优

GC 模块常用操作

import gc

# 1. 查看 GC 状态
print(gc.isenabled())      # True/False
print(gc.get_threshold())  # (700, 10, 10)
print(gc.get_count())     # 每代当前对象数

# 2. 关闭 GC(仅引用计数生效)
gc.disable()

# 3. 调整阈值
gc.set_threshold(700, 10, 10)

# 4. 获取所有被 GC 跟踪的对象
print(len(gc.get_objects()))  # 小心!可能非常多

# 5. 查看垃圾对象(不可达但无法释放的)
print(gc.garbage)

# 6. 设置 debug 标志
gc.set_debug(gc.DEBUG_LEAK)     # 打印泄漏信息
gc.set_debug(gc.DEBUG_STATS)    # 打印统计信息
gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE)

弱引用:主动打破循环

import weakref

class Parent:
    def __init__(self):
        self.child = None

class Child:
    def __init__(self, parent):
        self.parent = weakref.ref(parent)  # 弱引用!

p = Parent()
c = Child(p)
p.child = c
# 现在可以正常回收了:child 不增加 parent 的引用计数
del p  # Parent 引用计数归零,可正常释放

# 同样:weakref.WeakSet, weakref.WeakValueDictionary
弱引用不增加引用计数。典型的场景是父子关系:子对象通过弱引用持有父对象,这样父对象的生命周期不受子对象影响。这在缓存、观察者模式中非常常用。

交互演示:循环引用 vs 弱引用

对比两种情况下对象能否被正常回收。

class Node:
    def __init__(self, name):
        self.name = name
        self.ref = None
    def __del__(self):
        print(f"{self.name} 被销毁")

a = Node("A")
b = Node("B")
a.ref = b
b.ref = a  # ← 循环引用

del a, b  # 不会立即触发 __del__
gc.collect()  # ← GC 扫描:A 被销毁, B 被销毁
import weakref

class Node:
    def __init__(self, name):
        self.name = name
        self.ref = None
    def __del__(self):
        print(f"{self.name} 被销毁")

a = Node("A")
b = Node("B")
a.ref = weakref.ref(b)    # ← 弱引用,不增加计数
b.ref = weakref.ref(a)

del a, b  # 引用计数归零 → __del__ 立即触发
# 无需等待 GC!

七、内置小对象缓存机制

除了 pymalloc,Python 还对一些最常用的对象做了特殊的缓存优化,进一步减少内存分配。

小整数缓存

# -5 到 256 之间的小整数被预缓存
a = 100
b = 100
print(a is b)  # True! 同一个对象

a = 1000
b = 1000
print(a is b)  # False! 不同对象

CPython 启动时预创建 -5 到 256 的所有整数对象,高频使用,零分配开销。

字符串 intern 机制

# 短字符串可能被 intern 共享
a = "hello"
b = "hello"
print(a is b)   # True

# 手动 intern
import sys
c = sys.intern("large_str")

编译期相同的字符串字面量共享同一对象,运行期可用 sys.intern() 手动启用。


总结

Python 的内存回收是三层次的协作体系:

第一层
引用计数 · 主防守
确定性回收,引用计数归零立即释放,处理 95%+ 的回收场景
第二层
分代 GC · 兜底
检测并打破循环引用,分代策略减少扫描开销
第三层
pymalloc · 底层加速
≤512B 小对象走 pool 快速分配,减少系统调用
一句话记住:引用计数是主力,它让对象在没人用时瞬间释放;GC 是补丁,专门解决循环引用这个引用计数的漏洞;pymalloc 是基础设施,让频繁的小对象分配不再成为性能瓶颈。三块拼图合在一起,才构成了 Python "自动内存管理" 的完整图景。

Python 内存回收机制详解 · 基于 CPython 3.x 实现