在上一篇文章中,我们剖析了 Python 内存管理的第一道防线------引用计数。它实时、高效,却在"循环引用"面前束手无策。如果 Python 只有引用计数,那么复杂的对象网络将变成内存泄露的重灾区。
为了补齐这块短板,Python 引入了第二道防线:垃圾回收(Garbage Collection, GC)机制,主要通过"标记-清除"和"分代回收"技术,扫除那些躲在角落里的"僵尸对象"。
1. 致命的环:引用计数的"灯下黑"
我们先用一段代码现场还原引用计数的"无力感"。
Python
python
import gc
import sys
class Node:
def __init__(self, name):
self.name = name
self.next = None
# 1. 构造循环引用
a = Node("A")
b = Node("B")
a.next = b
b.next = a
# 2. 查看当前引用计数(通常是 2:变量引用 + next 指针引用)
print(f"A 的计数: {sys.getrefcount(a) - 1}")
# 3. 销毁外部引用
del a
del b
发生了什么? 此时,变量 a 和 b 已经从作用域消失,但对象 A 的 next 指向 B,对象 B 的 next 指向 A。它们的引用计数都停留在 1。
在引用计数眼中,它们"还没死";但在程序员眼中,它们已经变成了无法触达的内存垃圾。这就是所谓的**"死亡环联"**。
2. 分代回收(Generational GC):内存的"达尔文进化论"
为了处理这些顽固垃圾,同时又不至于频繁扫描整个内存导致卡顿,Python 采用了分代回收 策略。其核心思想是:存活时间越久的对象,越不可能是垃圾。
Python 将内存中的对象分为三代:0 代(Generation 0) 、1 代(Generation 1) 和 2 代(Generation 2) 。
① 0 代:新生力量的练兵场
所有新创建的对象都会被放入 0 代。由于大多数对象都是"朝生夕死"(如局部变量),0 代的扫描频率最高。
- 触发时机:当新创建的对象数量减去被销毁的对象数量,达到阈值(默认 700)时,触发 0 代回收。
② 1 代:中坚力量的过渡
如果一个对象在 0 代回收中幸存下来(即它还有效),它就会被"晋升"到 1 代。
- 触发时机:当 0 代回收触发了 10 次,才会带动 1 代进行一次回收。
③ 2 代:长生不老的归宿
在 1 代回收中依然屹立不倒的对象,最终进入 2 代。这里存放的是全局变量、模块对象等长生命周期对象。
- 触发时机:扫描频率极低。只有当 1 代回收也触发了多次(默认 10 次)后,才会触发全量(Full GC)回收。
3. 深度点:为什么大数据处理要手动 gc.collect()?
在处理海量数据(如从数据库加载千万级记录)时,你会发现内存占用在短时间内剧增,但 Python 的自动回收似乎"反应迟钝"。
阈值滞后问题
Python 的自动 GC 是基于对象数量 (Threshold)的,而不是基于内存占用大小的。
- 如果你创建了 699 个占用 1GB 内存的大型对象,GC 不会触发(因为没到 700 的阈值)。
- 如果你创建了 701 个占用 1KB 内存的小对象,GC 反而会频繁工作。
显式干预的场景
在删除大型数据结构或结束大规模循环后,手动调用 gc.collect() 可以强制清理 0 代、1 代甚至 2 代中的循环引用,将内存归还给操作系统。
Python
python
import gc
# 处理完大量数据后
del big_data_list
# 强制进行全量垃圾回收,打破可能的循环引用
gc.collect()
4. 性能调优:gc.set_threshold 的艺术
高级开发者可以通过调整回收阈值,在"回收频率"与"程序性能"之间寻找平衡。
Python
bash
# 获取当前阈值,默认通常是 (700, 10, 10)
print(gc.get_threshold())
# 调优:在高并发、短对象极多的场景下,可以调大 0 代阈值
# 减少 GC 扫描导致的 CPU 抖动
gc.set_threshold(2000, 15, 15)
调优逻辑:
- 增加阈值:减少 GC 频率,提升程序运行速度,但会增加内存峰值占用。
- 减少阈值:更频繁地释放内存,但会牺牲一部分 CPU 性能去运行 GC 扫描。
5. 优雅的代码不制造僵尸
虽然有 GC 兜底,但作为全栈/高级开发,我们的目标应该是:尽量不产生循环引用。
- 弱引用(weakref) :如果两个对象必须互相感知,考虑让其中一个使用
weakref.ref。弱引用不会增加引用计数,也不会阻碍对象被回收。 - 避免
__del__的坑 :在老版本 Python 中,定义了__del__方法的对象若涉及循环引用,GC 无法自动处理。虽然 Python 3.4+ 解决了这个问题,但依然建议慎用__del__。 - 局部变量化:尽量缩短对象的生命周期,让它们在 0 代就被引用计数"即刻处决",不要给它们晋升到 2 代的机会。