6月2日 01:36

Python 内存管理是怎样的?引用计数、分代 GC 和内存池原理

Python 内存管理分三层:引用计数(主要)、垃圾回收(处理循环引用)、内存池(减少 malloc 开销)。日常开发不需要手动管理内存,但理解机制能帮你排查内存泄漏。

引用计数:核心机制

每个对象维护一个引用计数 ob_refcnt。引用增加时 +1,引用减少时 -1,归零时立即释放。

python
import sys a = [1, 2, 3] # 引用计数 1 b = a # 引用计数 2 c = a # 引用计数 3 print(sys.getrefcount(a)) # 4(多 1 是因为 getrefcount 参数本身也是引用) del b # 引用计数 2 c = None # 引用计数 1 # a 离开作用域后引用计数归零,内存释放

引用计数的优势:实时释放,不需要暂停程序做垃圾回收。劣势:无法处理循环引用。

循环引用问题

python
a = [] b = [] a.append(b) # a 引用 b b.append(a) # b 引用 a del a, b # 引用计数各剩 1(互相引用),永远不会归零

引用计数对循环引用无能为力。Python 用分代垃圾回收(GC)处理这种情况。

分代垃圾回收

GC 把对象分成三代:第 0 代(新对象)、第 1 代、第 2 代(长寿对象)。

  • 新创建的对象在第 0 代
  • 经过一次 GC 存活的对象晋升到下一代
  • 第 0 代 GC 最频繁(阈值约 700 个对象),第 2 代最少

分代回收的理论依据:大部分对象很快变成垃圾(如函数内的临时变量),长寿对象倾向于一直活着。只频繁检查年轻对象,减少 GC 开销。

python
import gc print(gc.get_threshold()) # (700, 10, 10) — 第0代阈值700,每10次第0代GC触发1次第1代 print(gc.get_count()) # 当前各代对象计数

手动触发 GC:gc.collect()。通常不需要手动调用,但在处理大量循环引用对象后可以主动回收。

内存池:pymalloc

Python 不直接用系统的 malloc/free 管理小对象,而是用自己实现的 pymalloc 内存池:

  • 小对象(<512 字节):由 pymalloc 从大块内存中分配,减少系统调用
  • 大对象(>=512 字节):直接用系统 malloc

内存池按 256 KB 的 arena 分块,arena 内按 4 KB 的 pool 分块,pool 内按固定大小的 block 分配。相同大小的 block 共享 pool,减少碎片。

这就是为什么 Python 进程的 RSS(常驻内存)不会随着对象释放而下降——pymalloc 保留空闲 arena 供后续使用,不归还给操作系统。

内存泄漏排查

Python 的内存泄漏通常是"对象被意外引用导致无法释放"而非真正的泄漏。

1. 用 objgraph 找到引用链

python
import objgraph objgraph.show_backrefs(objgraph.by_type("dict")[0], max_depth=5)

生成引用关系图,找到谁在持有不该持有的引用。

2. 用 tracemalloc 定位分配位置

python
import tracemalloc tracemalloc.start() # ... 运行代码 ... snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics("lineno") for stat in top_stats[:10]: print(stat)

显示内存分配最多的代码行。

3. 常见泄漏原因

  • 全局列表/字典不断追加但从不清理
  • 闭包或回调持有大对象引用
  • __del__ 方法导致对象无法被 GC 回收(Python 3.4 已改善)
  • 缓存没有上限(用 functools.lru_cache 替代手动缓存)

追问

为什么 Python 进程的内存只增不减?

pymalloc 保留空闲 arena 不归还操作系统。如果你看到 RSS 很高但对象不多,可能是内存碎片。malloc_trim(0) 可以强制归还(Linux),但不保证生效。

引用计数和 GC 哪个先执行?

引用计数是实时的,每次赋值/删除都更新。GC 是周期性运行的,只处理循环引用。两者配合工作。

标签:Python