引用计数与垃圾回收

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 分代来补刀。

相关链接