Python内存管理:引用计数机制、垃圾回收(GC)、循环引用问题

聊到Python的内存管理,很多人第一反应就是「Python有垃圾回收,不用管内存」。这话对了一半。我见过不少线上服务,就是因为对内存管理一知半解,结果内存涨上去就下不来,最后OOM被kill掉。

说白了,Python的内存管理就三板斧:引用计数、标记-清除、分代回收。咱们一个一个拆开看。

引用计数——最基础的记账本

每个Python对象内部都维护着一个整数,叫ob_refcnt。你每多一个变量指向它,这个数就+1;少一个引用,就-1。当计数归零,内存立即释放。

import sys

a = []
print(sys.getrefcount(a))  # 2,getrefcount自己也会加一次引用

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

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

这个机制的好处是实时、简单。坏处呢?循环引用——两个对象互相指着,外部没人用了,但引用计数永远不为0。

核心要点:引用计数是Python内存管理的基石,但它搞不定循环引用。这就是GC存在的意义。

循环引用——那个经典的坑

我在项目中遇到过最典型的场景:一个缓存系统,用字典存对象,对象里又持有字典的引用。跑着跑着,内存就炸了。

class Node:
    def __init__(self):
        self.parent = None
        self.children = []

def create_cycle():
    a = Node()
    b = Node()
    a.children.append(b)
    b.parent = a  # 循环引用形成
    return a, b

# 函数返回后,a和b的外部引用没了
# 但a和b互相指着,引用计数都是1
# 内存泄漏了

你想想看,这种代码在业务里太常见了。树结构、图结构、双向链表、回调闭包……稍不注意就埋了雷。

避坑指南:我曾经排查过一个内存泄漏问题,查了两天才发现是ORM模型对象之间互相引用了。后来加了个weakref就解决了。记住:能用弱引用的地方,别用强引用。

垃圾回收——Python的清洁工

Python的GC主要负责处理引用计数搞不定的循环引用。它用的是标记-清除算法,配合分代回收来提升效率。

标记-清除(Mark-Sweep)

流程其实不复杂:

  1. 从根对象(全局变量、调用栈等)出发,遍历所有可达对象,打上标记
  2. 遍历堆内存,把没标记的对象——也就是不可达的——清掉

嗯,这里要注意:标记-清除会暂停程序执行。虽然Python做了优化,但如果你的对象数量巨大,这个停顿还是能感觉到的。

分代回收(Generational GC)

Python把对象分成三代:

代 特点 回收频率
第0代 新创建的对象 最频繁
第1代 熬过一次GC的对象 中等
第2代 熬过多次GC的对象 最低

这个设计的直觉是:大部分对象活不长。你想想看,函数里的临时变量,用完就扔。真正能活到第2代的对象,大概率会一直活着。所以没必要频繁扫描它们。

import gc

# 查看当前各代的对象数量
print(gc.get_count())  # (第0代数量, 第1代数量, 第2代数量)

# 手动触发一次GC
gc.collect()

# 调整阈值:第0代对象超过700时触发GC
gc.set_threshold(700, 10, 10)

个人习惯:我一般不会手动调GC参数,除非真的遇到性能瓶颈。但我会在关键业务代码里显式调用gc.collect()——比如处理完一批大对象后,主动清理一下,心里踏实。

GC调优——什么时候该出手

说实话,大部分Python应用不需要调GC。但如果你遇到以下情况,就得动手了:

  • 内存持续增长,但业务逻辑没有明显泄漏
  • GC停顿导致接口响应时间抖动
  • 大量短生命周期对象频繁创建销毁

我建议的排查步骤:

  1. 先开gc.set_debug(gc.DEBUG_LEAK),看看哪些对象没法回收
  2. 用gc.get_objects()按类型统计,找到异常聚集的类
  3. 检查是不是有循环引用,能用weakref就换
  4. 实在不行,调大GC阈值,减少回收频率
import gc
import collections

# 统计每种类型的对象数量
obj_counts = collections.Counter(type(o).__name__ for o in gc.get_objects())
for name, count in obj_counts.most_common(20):
    print(f"{name}: {count}")

注意:别把GC完全关掉。我见过有人为了性能调优,直接gc.disable(),结果内存泄漏到系统崩溃。GC是安全网,不是性能瓶颈。

一张图看懂Python内存管理

下面这张图把引用计数、GC、循环引用的关系串起来了:

Python内存管理核心流程 Python内存管理核心流程 Python对象 引用计数 (ob_refcnt) 引用计数 == 0? 立即释放内存 进入GC检测 分代回收判断 标记-清除算法 引用计数解决大部分场景,GC处理循环引用,分代回收提升效率

实战建议

最后,给几个我踩坑换来的建议:

  • 能用__slots__就用:减少每个对象的__dict__开销,省内存也省GC压力
  • 大对象用完手动置None:帮助引用计数尽快归零
  • 监控gc.get_count():如果第0代对象数量异常高,说明有大量临时对象在折腾
  • 别迷信gc.disable():除非你100%确定没有循环引用,否则别关

我个人习惯在性能测试环境里,先跑一遍gc.set_debug(gc.DEBUG_STATS),看看GC触发的频率和耗时。数据说话,比瞎猜靠谱得多。