Python 对象“死“了之后去了哪?——引用计数与分代垃圾回收

Python 对象"死"了之后去了哪?------引用计数与分代垃圾回收

Python · 内存管理。这篇文章讲引用计数的原理、循环引用这个老大难、以及分代垃圾回收怎么兜底,还有 sys.getrefcount 和 weakref 这两个实用工具。

一个"内存悄悄涨"的怪现象

有段服务跑了一周,内存从 200MB 慢慢涨到 2GB,最后 OOM 被杀。我查了半天,发现是几处数据结构"互相指着对方",谁也离不开谁,却谁也再也用不上------典型的循环引用。

这让我开始好奇:Python 到底是怎么判断"一个对象没用了,可以回收了"的?为什么大多数时候它收得干干净净,偏偏循环引用就漏了?答案藏在一套引用计数 + 分代回收的机制里。这篇文章把它讲透。

引用计数:最朴素的"记账"

Python 里每个对象头上都挂着一个计数器,叫 ob_refcnt,记录"当前有多少个引用指向我"。规则简单到粗暴:

text 复制代码
创建对象 → 计数为 1
被赋值/被引用 → 计数 +1
引用消失(变量删除、重新赋值、离开作用域)→ 计数 -1
计数归 0 → 立刻回收,内存还给系统

你可以用 sys.getrefcount 观察它:

python 复制代码
import sys

a = []            # 创建一个空列表
print(sys.getrefcount(a))   # 2,而不是 1!

b = a             # b 也指向它
print(sys.getrefcount(a))   # 3

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

等等,为什么第一个打印是 2?因为 getrefcount(a) 在把 a 传进函数的那一刻,参数本身又增加了一个临时引用。所以读到的值,永远比"你以为的"多 1------这是新手常踩的小坑。

引用计数的好处是即时 :对象一没人用,立刻回收,绝不拖延。缺点也很明显:每次赋值、删除都要更新计数器 ,有开销;更致命的是------它解决不了循环引用。

循环引用:计数永远归不了零

看这个:

python 复制代码
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b        # b 的计数 +1
b.ref = a        # a 的计数 +1

del a
del b            # 两个对象从外部都"没人用"了,但内部还互相指着
                 # 计数都还是 1,永远归不了零 → 内存泄漏

a 和 b 像两个手拉手的人,外面没人叫他们了,但他们互相攥着,谁也走不了。引用计数对此无能为力。这就是开头那个内存悄悄上涨的根源。

分代回收:给循环引用兜底

Python 的解法是加一道分代垃圾回收(generational GC),专门扫描循环引用。它的核心假设是:

活得越久的对象,越可能继续活下去;刚创建的对象,更可能是"短命"的。

于是它把对象按"年龄"分成三代:

text 复制代码
第 0 代:刚创建的对象(最年轻)
第 1 代:熬过一轮回收的幸存者
第 2 代:老家伙(活得最久)

回收频率也分代:第 0 代扫得最勤,第 1 代次之,第 2 代最懒。 默认阈值是(700, 10, 10)------第 0 代对象数超 700 就触发一次 0 代回收,0 代回收 10 次才触发一次 1 代回收,以此类推。

扫描时,GC 用**"可达性分析"**:从一个"根集合"(栈、全局变量等)出发,能走到标记为"活",走不到的,即使互相引用,也一并回收。于是 a.ref = b; b.ref = a 这种死循环,就被整片端掉了。

你可以用 gc 模块手动干预:

python 复制代码
import gc

gc.collect()              # 手动触发一次全量回收
print(gc.get_threshold()) # (700, 10, 10)
gc.disable()              # 关掉自动 GC(高性能场景有人这么做,但需谨慎)

weakref:想引用,又不想"拖累"它

有时候你确实需要引用一个对象,但不想因此阻止它被回收 ------比如缓存里"有就用,没有就算了"。这时用弱引用:

python 复制代码
import weakref

class Obj:
    pass

o = Obj()
r = weakref.ref(o)   # 弱引用:不增加引用计数
print(r())           # 返回对象 <...Obj...>

del o                # 强引用没了,对象被回收
print(r())           # None

weakref 的原理是绕开引用计数 :弱引用不增加 ob_refcnt,所以对象该回收时照常回收。functools.lru_cache、观察者模式、缓存库背后,都能看到它的身影。

所以,这件事平时要不要管

大多数时候,不用管。引用计数 + 分代回收已经把 99% 的场景料理得妥妥当当,你只管写代码。真正需要你动脑的,是那几个少数时刻:

  • 对象互相引用、形成环,且实现了 __del__(析构函数会干扰 GC 的判断,历史上是个坑);
  • 长时间运行的服务,内存缓慢上涨------八成是哪里藏了个循环引用,或某个全局容器越攒越大;
  • 极致性能场景,想关掉自动 GC、改用手动 gc.collect() 卡点回收。

看懂这套机制,不是为了天天调它,而是为了在内存出问题时,知道该往哪儿看。等你哪天被一个"怎么也降不下来"的内存曲线折磨过,就会感谢今天把这篇文章读完了。

引用计数、分代回收背后是 Python 对象模型的一部分,想系统理解 Python 底层,B站【408实验室】的《Python 完全自学教程》里有从基础到进阶的完整路径。

相关推荐
CCblandy1 小时前
雪花 ID 四个边界实测:时钟回拨重号 500 个,前端 JSON.parse 直接丢精度
后端·python·算法·性能优化·架构
quantdash_cc1 小时前
Pandas 处理分钟级股票行情:从原始数据清洗到 K 线聚合
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
yu_zhidun1 小时前
企业电脑监控软件怎么选:别只看功能清单,先看这 6 个底层能力
开发语言·电脑·php·域智盾系统
把我格子衫拿来1 小时前
VTK 9.5 与 Qt 6 联合开发:从嵌入三维窗口到可维护的桌面架构
c++·python·qt·vtk·三维可视化
cpolar技术支持2 小时前
把配置发出去怕带出密钥?先把含凭据配置转成脱敏只读快照,用 cpolar 给同事核对差异
运维·python·内网穿透·cpolar·配置脱敏
无忧.芙桃2 小时前
C++语言原理与实践(十六):多态机制讲解
开发语言·c++·多态
惊讶的猫2 小时前
langchian_agent的高级使用
python
李纲明2 小时前
WordPress 白屏排查手册:关插件、换主题、开调试、查 PHP(附停手信号)
开发语言·php·wordpress·故障排查·白屏·插件冲突
ShineWinsu3 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环