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

聊到Python的内存管理,很多人第一反应就是「Python不是有自动垃圾回收吗?我不用管内存」。嗯,这话对了一半。自动回收确实省心,但你要是写过长时间运行的服务,或者处理过大量数据,就会明白——内存泄漏这玩意儿,Python里一样存在。

我个人习惯把Python的内存管理拆成三个层面来看:引用计数、垃圾回收、以及那个最让人头疼的循环引用。咱们一个一个说。

引用计数:最基础的内存管理手段

Python里每个对象都有一个引用计数。说白了,就是记录「有多少个变量在指着这个对象」。当引用计数降到0,内存就会被立即释放。

看个简单的例子:

import sys

a = []
print(sys.getrefcount(a))  # 输出 2(a 和 getrefcount 的临时引用)

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

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

我在项目中遇到过一个问题:一个对象被多个地方引用,但某个引用忘记释放了,导致对象一直活着。这种问题用 gc.get_objects() 配合 sys.getrefcount 就能定位。

小技巧: 用 gc.get_referrers(obj) 可以查看谁在引用某个对象,排查泄漏时特别好用。

垃圾回收:引用计数的补充

引用计数有个硬伤——它搞不定循环引用。你想想看,两个对象互相指着对方,外部又没有变量引用它们,引用计数永远到不了0。这时候就需要垃圾回收器出马了。

Python的垃圾回收器主要做两件事:

  • 检测循环引用:通过「标记-清除」算法,找出那些互相引用但外部不可达的对象
  • 分代回收:把对象按存活时间分成三代,越老的对象越少被扫描

我建议你记住这个分代参数:

import gc

# 查看当前阈值
print(gc.get_threshold())  # 默认 (700, 10, 10)

# 手动触发垃圾回收
gc.collect()

第一代触发阈值是700,意思是新分配对象数减去回收对象数达到700时,就会触发一次第0代回收。第二代和第三代则是基于前一代的回收次数。

核心要点: 分代回收的设计思路是「大多数对象都是短命的」。年轻代频繁扫描,老年代少扫描,这样性能最优。

循环引用:最常见的泄漏元凶

循环引用长什么样?我直接给你看代码:

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

def create_cycle():
    a = Node("A")
    b = Node("B")
    a.children.append(b)
    b.parent = a
    # 函数结束,a和b互相引用,但外部没有引用它们
    # 如果没有垃圾回收,这两个对象永远不会被释放

create_cycle()
# 此时 a 和 b 已经不可达,但引用计数不为0

我曾经在一个爬虫项目里遇到过类似问题。每个请求对象都持有response对象的引用,response又持有request的引用,结果内存蹭蹭往上涨。排查了整整一天,最后发现是回调函数里不小心形成了循环引用。

避坑指南: 我曾经在类中定义了 __del__ 方法,结果循环引用导致 __del__ 永远不会被调用。因为垃圾回收器无法确定循环引用中对象的销毁顺序。所以,尽量不要在 __del__ 里写重要逻辑。

如何检测和解决循环引用

检测循环引用,我一般用 gc 模块:

import gc

# 开启垃圾回收的调试
gc.set_debug(gc.DEBUG_SAVEALL)

# 创建循环引用
class A:
    def __init__(self):
        self.b = None

class B:
    def __init__(self):
        self.a = None

a = A()
b = B()
a.b = b
b.a = a

# 删除外部引用
del a
del b

# 手动触发回收
gc.collect()

# 查看被回收的对象
for obj in gc.garbage:
    print(type(obj), repr(obj))

解决循环引用,常用的方法有:

  • 使用弱引用:用 weakref 模块,不增加引用计数
  • 显式断开引用:在适当的时候手动设置 None
  • 使用上下文管理器:确保资源被正确释放

弱引用的用法:

import weakref

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

a = Node("A")
b = Node("B")

# 使用弱引用,不增加引用计数
b.parent = weakref.ref(a)
a.children.append(b)

# 现在即使删除 a,也不会因为循环引用导致内存泄漏

知识体系总览

下面这张图,是我梳理的Python内存管理核心逻辑。你看一眼就能明白整个体系是怎么串起来的:

Python内存管理核心逻辑 Python对象 引用计数 (Reference Counting) 计数为0 立即释放内存 循环引用 垃圾回收器介入 分代回收 (Generational GC) 第0代 第1代 第2代 标记-清除 (Mark & Sweep) 核心原则:引用计数为主,分代回收为辅,标记-清除解决循环引用

说白了,Python的内存管理就是一套组合拳。引用计数负责快速释放,垃圾回收负责兜底,分代回收负责优化性能。你只要记住:大部分情况下你不需要操心,但一旦出现内存泄漏,先查循环引用。

实战建议: 我一般在写长时间运行的服务时,会在关键位置加上 gc.get_objects() 的监控,定期打印对象数量。一旦发现某个类的实例数异常增长,立刻就能定位问题。

好了,关于Python内存管理的三个核心概念,咱们就聊到这儿。记住一句话:引用计数是基础,垃圾回收是保障,循环引用是坑。下次你遇到Python进程内存只涨不降,先想想是不是哪个地方互相指着没放开。

可可精品资料库