人人都会AI编程

4.3 垃圾回收机制

更新时间:2026-07-12

你可能会想:Python 不是已经用引用计数来管理内存了吗,为什么还需要垃圾回收?

因为引用计数有一个致命的盲区——循环引用。

循环引用:引用计数的软肋

试想一个场景:两个对象互相引用对方,但外部没有任何变量再指向它们。

a = []
b = []
a.append(b)   # a 引用 b
b.append(a)   # b 引用 a
del a
del b

执行 del adel b 后,两个列表对象各自的引用计数都变成 1(因为它们互相引用,不归零),但它们实际上已经无法从代码中访问到了(变成了“漂浮在内存中的垃圾”)。单靠引用计数,这些对象永远不会被释放,造成内存泄漏

Python 的解决方案是,在引用计数之外,再引入一个垃圾回收器(GC),专门用来揪出并清理这种循环引用。

标记-清除算法(Mark and Sweep)

这是垃圾回收器的核心算法,分为两步:

  1. 标记阶段:从根对象(root objects)出发,遍历所有可达对象,给它们打上“存活”标记。根对象包括全局变量、函数栈上的局部变量、活动线程等直接可达的入口。只要从根对象能顺着引用链走到某个对象,它就是“活着的”。
  2. 清除阶段:遍历堆内存中的所有对象,把没有被标记的对象认定为不可达垃圾,回收它们占用的内存。

在上面的循环引用例子中,两个列表对象都无法从任何根对象到达,所以都不会被标记,最终在清除阶段被回收。

关键点:这个回收过程只用来处理那些可能包含循环引用的容器对象(比如 listdictset、自定义类实例等)。对于整数、字符串等不可变的基本类型,无论如何都不会形成循环引用,所以 Python 压根不会去扫描它们,避免了不必要的性能开销。

分代回收(Generational Collection)

如果每次都对所有容器对象执行一遍完整的标记-清除,代价很高。Python 引入了一个优化策略——分代回收,基于一个经验观察:大多数对象都是“短命鬼”,存活越久的对象越可能继续活下去。

Python 把对象按存活时间分为三代:

  • 零代(Young Generation):新创建的对象。回收最频繁,频率和执行时间都短。
  • 一代(Middle Generation):从零代回收中幸存下来的对象。
  • 二代(Old Generation):存活时间最长的对象,回收频率最低。

触发规则:

  • 当一个代中分配的对象数量超过阈值时,触发该代的垃圾回收。
  • 回收先发生在零代。如果在零代回收时发现某些对象存活,就把它们“晋升”到一代;当一代对象数量也超过阈值时,才会触发一代回收,并可能继续晋升到二代。
  • 每一代的回收都会连带对更年轻的代进行回收(回收一代时会顺带回收零代)。

实际意义:绝大部分临时对象会在零代就被快速清理掉,系统不用频繁扫描庞大且长期存活的老对象,大大降低了 GC 的总开销。

开发者需要做什么?

在绝大多数情况下,你完全不需要手动干预。Python 的 GC 在后台默默工作,一切都自动且高效。但有两点值得知道:

  • 可以手动触发gc.collect() 可强制立即执行一次完整的垃圾回收,偶尔在性能测试或内存压力敏感的场景中使用。
  • 可以关闭自动回收gc.disable() 会暂停自动 GC。当程序大量创建会产生循环引用的短命对象时,关闭自动回收、改为在批处理结束时手动回收,有时能提升性能并减少回收停顿。但这属于高阶优化,多数项目无需触碰。
  • 标准库模块gc 模块提供了查看回收统计、设置阈值、调试循环引用等接口,是排查内存问题的利器。

简单总结:引用计数负责实时释放绝大部分对象,而垃圾回收器用标记-清除清除循环引用垃圾,用分代回收降低扫描成本。两者分工协作,让 Python 的内存管理既高效又省心。