引用计数 · 分代垃圾回收 · 小对象池 —— 这三块拼图共同构成了 Python 高效的内存管理
Python 的内存管理像一个精密的管家系统——它会自动跟踪每个对象被引用了多少次,在没人用的时候立即回收;对于循环引用这种引用计数搞不定的情况,它还有一套独立的分代垃圾回收器;而对大量小块对象的频繁分配,它直接向操作系统批发内存,再在内部做精细管理。下面逐一拆解这三块拼图。
这是 Python 内存回收的第一道防线,也是最主要的机制。每个 Python 对象在内存中都带有一个 ob_refcnt 字段,记录当前有多少个引用指向它。当引用计数降到 0 时,对象被立即销毁,内存归还。
赋值给新变量、作为参数传递、加入容器(list/dict/set)、作为属性赋值
变量被重新赋值(指向其他对象)、del 删除变量、离开作用域、容器被销毁
调用 __del__ 方法(如果定义了),然后 C 层面调用 tp_dealloc 释放内存块
点击按钮模拟 Python 中对一个列表对象的引用操作,观察引用计数的变化。
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
为了解决循环引用的问题,Python 引入了分代垃圾回收器(Generational Garbage Collector)。它的核心思想来自一个观察:大多数对象生命周期很短,少数对象存活很久。
gc.get_threshold() 查看和 gc.set_threshold() 调整。默认是 (700, 10, 10) —— 即第 0 代每新增约 700 个对象触发一次 GC,第 0 代每 10 次 GC 触发一次第 1 代 GC,以此类推。
Python 的 GC 运行在 gc 模块中,它的检测算法分三步:
GC 只关心可能产生循环引用的容器:list、dict、set、tuple、自定义类的实例等。int、str 等不可变基本类型不会被循环引用困住,直接跳过。
GC 把每个容器对象的 ob_refcnt 复制一份,然后遍历每个容器的子对象,将子对象的复制版本引用计数减 1。这一步的目的是「消除内部循环的引用」。
扣除后引用计数仍大于 0 的说明有外部引用,保留。降为 0 的说明所有引用都来自这个循环内部——这些就是不可达的循环垃圾,全部回收。
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()) # 返回值:回收的对象数
Python 程序运行时会产生海量的小对象——整数、浮点数、短字符串、小列表等。如果每次都向操作系统申请和归还内存,开销巨大。Python 的做法是:向操作系统批发大块内存,然后在内部做精细化的零售管理。
| 层级 | 大小 | 职责 |
|---|---|---|
| Arena | 256 KB | 从操作系统申请的大块连续内存,一个 arena 包含多个 pool |
| Pool | 4 KB | 管理同一种 size class 的对象,每个 pool 只服务一种大小 |
| Block / Slot | 8B ~ 512B | 最小的分配单元,对应一个 Python 对象 |
malloc。这意味着 Python 里的 int、float、小 list、小 dict、小 tuple 等绝大多数对象都享受 pymalloc 的高效管理。
不是每次 new 一个对象都向 OS 要内存,而是预先批发一大块 arena,内部自己切分。
对象被释放后,slot 回到空闲链表。下次分配同样大小的对象时直接复用,无需重新申请。
每个 pool 只管理一种大小的 slot,不存在「释放了一个 16B 对象留下 16B 空隙,但下一个要分配 24B 放不下」的问题。
已释放的 slot 通过一个单链表连接,O(1) 取用。分配时从链表头拿,释放时插入链表头。
这三个机制不是在比赛,而是在分工协作。理解它们的协作关系,才算真正理解了 Python 内存回收。
正常路径:引用计数搞定一切 · 兜底路径:GC 处理循环引用 · 底层加速:pymalloc 减少系统调用
def process(): data = [1, 2, 3] return sum(data) # data 在函数返回后引用计数归零 # 内存立即被引用计数机制回收 # pymalloc 把 slot 放回空闲链表
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 打破循环 |
file.close() 在 del f 之后立即发生。在纯 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
对比两种情况下对象能否被正常回收。
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 共享 a = "hello" b = "hello" print(a is b) # True # 手动 intern import sys c = sys.intern("large_str")
编译期相同的字符串字面量共享同一对象,运行期可用 sys.intern() 手动启用。
Python 的内存回收是三层次的协作体系:
Python 内存回收机制详解 · 基于 CPython 3.x 实现