引用计数与垃圾回收
Python 内存管理 = 引用计数(主力)+ 垃圾回收(补刀)
一、引用计数(Reference Counting)
类比:图书馆借书卡
每本书后面有张卡,借一次盖个章(+1),还回划掉(-1)。卡上一个章都没了 → 书直接回收。
基本规则
import sys
a = [] # 空列表,引用计数 = 1(a 指着它)
b = a # 引用计数 = 2(a、b 都指着它)
b = None # 引用计数 = 1(b 不指了)
a = None # 引用计数 = 0 → 立即释放
-
被引用 →
refcount +1 -
引用失效 →
refcount -1 -
refcount == 0→ 立刻回收
哪些操作会 +1 / -1
# 哪些操作会 +1
a = [1, 2, 3] # 赋值
b = a # 别名引用
func(a) # 传参(函数内部引用)
lst = [a] # 放入容器
# 哪些操作会 -1
a = None # 变量重新赋值
del a # 显式删除
func() 执行完毕 # 局部变量出作用域
优点: 实时回收,内存不会堆积。
缺点: 循环引用搞不定。
二、循环引用——引用计数的盲区
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b # a → b
b.ref = a # b → a
del a # a 的引用计数 -1,但不是 0(b.ref 还指着它)
del b # b 的引用计数 -1,同样不是 0
# → 内存泄漏
类比
两个人互相指着对方说”他还需要我”,外面已经没人需要他俩了,但他俩都说自己还在被需要。引用计数破不了这个僵局。
三、垃圾回收(GC)——专门处理循环引用
分代回收(Generational GC)
核心思路: 大多数对象 “朝生暮死”,活得越久的越可能继续活下去。
| 代 | 含义 | 触发频率 |
|---|---|---|
| 第 0 代 | 新对象 | 最频繁 |
| 第 1 代 | 熬过 1 次 GC 的对象 | 较少 |
| 第 2 代 | 熬过 2 次 GC 的对象 | 最少 |
import gc
gc.get_threshold() # → (700, 10, 10)
# 第1代GC执行10次 → 触发一次第2代GC
GC 怎么检测循环引用(四步走)
第一步:找出候选对象。 只看容器类型(list、dict、set、自定义类实例),整数、字符串等不可变对象不可能参与循环引用,直接跳过。
第二步:创建临时计数器 gc_refs。 初始值 = 当前 refcount。
第三步:模拟拆掉内部引用。 遍历每个候选对象,把它内部指向其他候选对象的引用”假装拆掉”,把被指着的对象的 gc_refs 减 1。
第四步:判生死。
-
gc_refs > 0→ 外面还有人指着我 → 存活 -
gc_refs == 0→ 只有循环里的自己人指着我 → 垃圾,回收
图解
# 循环引用场景
a = [1, 2]; b = [3, 4]
a.append(b); b.append(a)
del a, b
# 初始 gc_refs:
A.gc_refs = 1 # 只剩 B 内部引用
B.gc_refs = 1 # 只剩 A 内部引用
# 模拟拆掉内部引用:
遍历A,A内部引用B → B.gc_refs = 0
遍历B,B内部引用A → A.gc_refs = 0
# 两个都是 0 → 垃圾,回收
对比:有外部引用时
a = [1, 2]; b = [3, 4]
a.append(b); b.append(a)
# gc_refs:
A.gc_refs = 2(变量a + B内部引用)
B.gc_refs = 1(A内部引用)
# 拆掉内部引用:
遍历A → B.gc_refs = 0
遍历B → A.gc_refs = 1 ← > 0,存活!
# A 可达 → A 引用的 B 也跟着可达 → 都不回收
四、__del__ 陷阱
class A:
def __init__(self):
self.ref = None
def __del__(self):
print("被删了")
a1 = A(); a2 = A()
a1.ref = a2; a2.ref = a1
del a1, a2 # __del__ 不会被调用!
原因: GC 不知道先删谁(互相指着),干脆不删,扔进 gc.garbage。
解决方法: 用 weakref(弱引用,不增加引用计数)
import weakref
class A:
def __init__(self):
self.ref = None
a1 = A(); a2 = A()
a1.ref = weakref.ref(a2) # 弱引用,不增加引用计数
a2.ref = weakref.ref(a1)
del a1, a2 # __del__ 正常执行
五、手动触发 GC
import gc
gc.collect() # 手动触发(一般不需要)
gc.disable() # 禁用GC(高性能场景用)
总结
| 引用计数 | GC | |
|---|---|---|
| 触发时机 | 随时,每次引用变化时 | 满足阈值条件时 |
| 处理什么 | 普通垃圾(99%) | 循环引用(1%) |
| 判定逻辑 | refcount == 0 → 回收 | 外部可达性分析 |
| 速度 | 快,实时 | 慢,偶尔触发 |
| 能否手动触发 | 不(自动) | gc.collect() |
一句话: 引用计数管”没人指我”的死法,GC 管”你们互相指不准算”的死法。两道防线,一快一慢,互补。
速记卡(面试闪卡)
Q1:一句话讲清「引用计数与垃圾回收」到底是什么?
A:Python 用引用计数主力回收(计数为0立即释放),再用分代 GC 补刀循环引用。
Q2:一、引用计数怎么工作 —— 怎么理解?
A:每个对象有张”借书卡”:被引用+1、引用失效-1,归零立刻释放,内存不堆积。赋值、别名、传参、入容器都+1;del、重赋值、出作用域-1。它实时但搞不定循环引用。
Q3:二、循环引用是盲区 —— 怎么理解?
A:a.ref=b 且 b.ref=a,del a、del b 后互相指着,计数永远=1,内存泄漏。就像两人互指”他还需求我”,外面早不需要他俩了,但僵局谁也破不了。
Q4:三、GC 怎么收拾循环引用 —— 怎么理解?
A:分代回收(新对象第 0 代最频繁,活得越久越久才扫);检测四步走:找容器候选→建 gc_refs→模拟拆内部引用→gc_refs=0 的就是垃圾。外部还指着(>0)的一律存活。
Q5:四、del 陷阱与 weakref —— 怎么理解?
A:循环引用里若对象有 del,GC 不知先删谁,干脆扔进 gc.garbage 不调 del。解法用 weakref(弱引用不增加计数),可正常回收。还可 gc.collect() 手动触发、gc.disable() 在高性能场景关掉。
Q6:核心速记主线有哪些?
-
引用计数:refcount=0 立即释放,实时
-
循环引用是引用计数的死穴
-
GC 分代回收 + 可达性分析补刀
-
有 del 的循环引用会进 gc.garbage
-
weakref 弱引用不增加计数,破循环
口诀
A:引用计数是借书卡,
计数归零就回家;
循环互指死结打,
GC 分代来补刀。
相关链接
-
📋 目录:00-Python
-
📚 学习清单:八股文学习路线图
-
🔗 可变与不可变对象
-
🔗 深浅拷贝
-
🔗 进程vs线程vs协程 — 操作系统视角的并发基础
-
🔗 JS异步与事件循环 — Python asyncio vs JS 事件循环
-
🔗 进程vs线程vs协程 — 引用计数机制与对象内存模型的关系