2. 垃圾回收机制

聊到Python的内存管理,很多人第一反应就是「Python有垃圾回收,不用管内存」。嗯,这话对了一半。Python确实有垃圾回收,但它不是万能的。我在生产环境里见过太多因为不懂GC机制而搞出来的内存泄漏,有的甚至把32G内存吃满,服务直接OOM挂掉。

说白了,Python的垃圾回收是三层架构:引用计数是主力,标记清除是救火队,分代回收是日常保洁。这三层配合起来,大部分场景都没问题。但每一层都有它的盲区,咱们一个一个说。

引用计数:最基础也最脆弱

Python里每个对象都有一个ob_refcnt字段,记录着有多少个引用指向它。当这个计数降到0,对象立即被回收。这是最直接、最实时的回收方式。

import sys

a = []
print(sys.getrefcount(a))  # 输出2,因为getrefcount本身也引用了a

b = a
print(sys.getrefcount(a))  # 输出3

del b
print(sys.getrefcount(a))  # 输出2

看起来很简单对吧?但这里有个坑:循环引用。两个对象互相引用,它们的引用计数永远不会降到0。

class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a

del a
del b
# 此时两个Node对象引用计数都是1,永远不会被回收

我在项目中遇到过这种情况:一个缓存系统里,对象之间互相持有引用,导致内存只增不减。当时查了两天才定位到问题,就是因为循环引用把引用计数给「锁死」了。

注意:引用计数无法处理循环引用。这是它最大的失效场景。如果你在代码里看到对象之间互相引用,一定要警惕内存泄漏。

标记清除:救火队也有局限

为了解决循环引用,Python引入了标记清除算法。它从根对象(比如全局变量、调用栈)出发,遍历所有可达对象,打上标记。没被标记的就是垃圾,直接清理。

这个过程分两步:

  1. 标记阶段:从根出发,遍历所有引用,标记可达对象
  2. 清除阶段:扫描所有对象,回收未标记的

听起来很完美?其实不然。标记清除有两个问题:

  • 性能开销大:需要遍历整个对象图,对象越多越慢
  • 会暂停程序:标记清除期间,Python解释器会暂停所有线程

我曾经在一个高并发服务里,因为对象数量太大,标记清除一次耗时好几秒,导致请求超时。嗯,那场面相当尴尬。

核心要点:标记清除能解决循环引用,但代价是性能。它不适合频繁触发,只适合作为「兜底」方案。

分代回收:日常保洁的艺术

Python把对象分成三代:

代 特点 回收频率
第0代 新创建的对象,存活时间短 最频繁
第1代 经过一次回收还活着的对象 中等
第2代 长期存活的对象 最低

这个设计基于一个经验规律:大部分对象都是朝生暮死的。你想想看,函数里的临时变量、循环里的中间结果,用完就扔。只有少数对象能活下来。

分代回收的策略很简单:

  • 第0代回收最频繁,因为垃圾最多
  • 第1代次之
  • 第2代很少回收,因为能活到这一代的对象通常都很「稳定」

这样做的好处很明显:大部分回收只发生在第0代,速度快、影响小。只有第0代回收次数达到阈值,才会触发第1代回收,以此类推。

小技巧:你可以通过 gc.get_threshold() 查看当前的回收阈值。默认是 (700, 10, 10),意思是第0代回收700次后触发第1代,第1代回收10次后触发第2代。

它们如何失效?

好了,现在咱们聊聊最核心的问题:这三层机制在什么情况下会失效?

引用计数失效的场景:

  • 循环引用(最常见)
  • 使用 __del__ 方法时,可能导致对象无法被回收
  • C扩展模块中手动管理引用计数出错

标记清除失效的场景:

  • 对象数量巨大时,标记清除本身会成为性能瓶颈
  • 如果对象图过于复杂,标记阶段可能耗尽CPU
  • 无法回收那些被C扩展模块「偷偷」引用的对象

分代回收失效的场景:

  • 长期存活的对象不断创建新引用,导致第2代对象越来越多
  • 如果程序创建对象的速度远大于回收速度,分代回收也扛不住
  • 某些对象虽然存活了很久,但其实是「僵尸对象」,占着内存不干活
避坑指南:我曾经在一个数据处理服务里,用 weakref 来避免循环引用。结果因为没处理好弱引用的回调,导致对象被提前回收,数据丢失。嗯,弱引用也不是银弹,用之前一定要想清楚生命周期。

一张图看懂Python垃圾回收

下面这张图展示了Python垃圾回收的整体流程。你可以看到引用计数是第一条防线,分代回收是日常维护,标记清除是最后的兜底方案。

Python垃圾回收机制流程图 根对象 引用计数 实时回收,无法处理循环引用 分代回收 第0代 → 第1代 → 第2代 日常保洁,频率递减 标记清除 兜底方案,解决循环引用 ❌ 循环引用失效 ❌ 对象创建过快失效 ❌ 对象过多导致性能问题

从这张图你可以看到,Python的垃圾回收是一个层层递进的过程。引用计数挡不住的问题,交给分代回收;分代回收搞不定的,最后由标记清除兜底。但每一层都有它的失效场景,了解这些场景,你才能写出真正内存安全的代码。

总结一下:引用计数是快刀,但怕循环引用;分代回收是日常保洁,但怕对象暴增;标记清除是救火队,但救火本身也有代价。理解这三者的关系,你就能在写代码时主动避开内存泄漏的坑。

可可精品资料库