你可能会想:Python 不是已经用引用计数来管理内存了吗,为什么还需要垃圾回收?
因为引用计数有一个致命的盲区——循环引用。
循环引用:引用计数的软肋
试想一个场景:两个对象互相引用对方,但外部没有任何变量再指向它们。
a = []
b = []
a.append(b) # a 引用 b
b.append(a) # b 引用 a
del a
del b
执行 del a 和 del b 后,两个列表对象各自的引用计数都变成 1(因为它们互相引用,不归零),但它们实际上已经无法从代码中访问到了(变成了“漂浮在内存中的垃圾”)。单靠引用计数,这些对象永远不会被释放,造成内存泄漏。
Python 的解决方案是,在引用计数之外,再引入一个垃圾回收器(GC),专门用来揪出并清理这种循环引用。
标记-清除算法(Mark and Sweep)
这是垃圾回收器的核心算法,分为两步:
- 标记阶段:从根对象(root objects)出发,遍历所有可达对象,给它们打上“存活”标记。根对象包括全局变量、函数栈上的局部变量、活动线程等直接可达的入口。只要从根对象能顺着引用链走到某个对象,它就是“活着的”。
- 清除阶段:遍历堆内存中的所有对象,把没有被标记的对象认定为不可达垃圾,回收它们占用的内存。
在上面的循环引用例子中,两个列表对象都无法从任何根对象到达,所以都不会被标记,最终在清除阶段被回收。
关键点:这个回收过程只用来处理那些可能包含循环引用的容器对象(比如 list、dict、set、自定义类实例等)。对于整数、字符串等不可变的基本类型,无论如何都不会形成循环引用,所以 Python 压根不会去扫描它们,避免了不必要的性能开销。
分代回收(Generational Collection)
如果每次都对所有容器对象执行一遍完整的标记-清除,代价很高。Python 引入了一个优化策略——分代回收,基于一个经验观察:大多数对象都是“短命鬼”,存活越久的对象越可能继续活下去。
Python 把对象按存活时间分为三代:
- 零代(Young Generation):新创建的对象。回收最频繁,频率和执行时间都短。
- 一代(Middle Generation):从零代回收中幸存下来的对象。
- 二代(Old Generation):存活时间最长的对象,回收频率最低。
触发规则:
- 当一个代中分配的对象数量超过阈值时,触发该代的垃圾回收。
- 回收先发生在零代。如果在零代回收时发现某些对象存活,就把它们“晋升”到一代;当一代对象数量也超过阈值时,才会触发一代回收,并可能继续晋升到二代。
- 每一代的回收都会连带对更年轻的代进行回收(回收一代时会顺带回收零代)。
实际意义:绝大部分临时对象会在零代就被快速清理掉,系统不用频繁扫描庞大且长期存活的老对象,大大降低了 GC 的总开销。
开发者需要做什么?
在绝大多数情况下,你完全不需要手动干预。Python 的 GC 在后台默默工作,一切都自动且高效。但有两点值得知道:
- 可以手动触发:
gc.collect()可强制立即执行一次完整的垃圾回收,偶尔在性能测试或内存压力敏感的场景中使用。 - 可以关闭自动回收:
gc.disable()会暂停自动 GC。当程序大量创建会产生循环引用的短命对象时,关闭自动回收、改为在批处理结束时手动回收,有时能提升性能并减少回收停顿。但这属于高阶优化,多数项目无需触碰。 - 标准库模块:
gc模块提供了查看回收统计、设置阈值、调试循环引用等接口,是排查内存问题的利器。
简单总结:引用计数负责实时释放绝大部分对象,而垃圾回收器用标记-清除清除循环引用垃圾,用分代回收降低扫描成本。两者分工协作,让 Python 的内存管理既高效又省心。