python垃圾回收以引用计数为主、标记-清除与分代回收为辅:引用计数实时释放内存但无法处理循环引用;标记-清除专治容器间循环引用;分代回收按对象存活年龄降低扫描开销。

Python 垃圾回收(GC)机制的核心是“引用计数为主,标记-清除与分代回收为辅”。面试中常考的不是背定义,而是能否讲清三种机制如何协同工作、各自解决什么问题、以及为什么缺一不可。
引用计数:最直接的内存释放信号
每个 Python 对象(如 int、list、class 实例)在 CPython 底层都对应一个 PyObject 结构体,其中字段 ob_refcnt 记录当前有多少引用指向它。
- 创建对象(
a = [1,2])、赋值(b = a)、入容器(lst.append(a))、传参等操作 → 引用计数 +1 -
del a、变量重新赋值(a = "new")、函数退出、从容器弹出或容器销毁 → 引用计数 -1 - 计数归零时,CPython 立即调用
tp_dealloc释放内存,无需等待 GC 轮询 —— 这就是它的实时性优势
但注意:sys.getrefcount(x) 本身会临时增加一次引用(因传参),结果比真实值大 1;且该机制对循环引用完全失效。
标记-清除:专治循环引用的兜底方案
引用计数无法处理的对象,主要是容器类型(list、dict、set、class 实例等)之间构成的闭环引用。例如:
立即学习“Python免费学习笔记(深入)”;
a = []<br>b = []<br>a.append(b)<br>b.append(a)
此时 a 和 b 的引用计数均为 1(彼此持有),但外部已无任何变量引用它们 —— 它们实际已“死亡”,却无法被引用计数回收。
- 标记-清除只作用于“容器对象”,因为只有容器才可能持有其他对象的引用并形成环
- 它不依赖计数,而是从根集(root set)出发:包括栈帧中的局部/全局变量、C 栈上的指针、寄存器等
- 先递归遍历所有可达对象并打上“存活”标记;再扫描全部容器对象,回收未被标记的 —— 即所谓“不可达垃圾”
分代回收:用年龄降低扫描开销
标记-清除若每次全量扫描所有对象,性能代价太高。CPython 引入三代分组策略,基于“越年轻越容易死”的经验规律:
- 新对象默认进入第 0 代;每次 GC 后仍存活的对象,会被移入更高代(0→1→2)
- 代越老,回收频率越低:0 代阈值默认 700(新增 700 个对象就触发),1 代默认每 10 次 0 代回收触发一次,2 代默认每 10 次 1 代回收触发一次
- 可通过
gc.get_threshold()查看,gc.set_threshold(300, 5, 5)可手动调优
这样既保证新对象快速清理,又避免反复扫描长期存活的大对象(如全局配置、单例实例)。
GC 模块:可控的调试与干预能力
面试官常问“怎么手动触发或监控 GC?”——关键就在 gc 模块:
-
gc.enable()/disable()控制开关(默认开启) -
gc.collect(generation=0)强制回收指定代,返回回收对象数量 -
gc.get_count()返回三元组(n0, n1, n2),即各代当前对象数 -
gc.get_objects(generation=2)获取某代所有被跟踪对象,适合排查内存泄漏 -
gc.set_debug(gc.DEBUG_STATS)开启日志,看到每次回收释放了多少对象
注意:手动调用 collect() 一般只用于调试或关键资源释放点(如大图处理后),生产环境不建议频繁调用。










