引用计数与分代回收
问题
Python 的垃圾回收机制是怎样的?引用计数和分代回收如何配合?循环引用怎么处理?
答案
引用计数(Reference Counting)
CPython 使用引用计数作为主要的内存管理机制:每个对象维护一个计数器记录有多少引用指向它。
import sys
a = [1, 2, 3] # 引用计数 = 1
b = a # 引用计数 = 2
print(sys.getrefcount(a)) # 3(getrefcount 本身也创建一个临时引用)
del b # 引用计数 = 1
del a # 引用计数 = 0 → 立即释放内存
引用计数变化时机:
| 操作 | 计数变化 |
|---|---|
赋值:b = a | +1 |
传参:func(a) | +1 |
加入容器:lst.append(a) | +1 |
del b | -1 |
| 变量离开作用域 | -1 |
引用被覆盖:b = other | -1 |
优点:实时回收,延迟低
缺点:无法处理循环引用,计数操作有性能开销
循环引用问题
# 循环引用:两个对象互相引用,引用计数永远不为 0
class Node:
def __init__(self, value):
self.value = value
self.next = None
a = Node(1)
b = Node(2)
a.next = b # a → b
b.next = a # b → a(循环引用)
del a, b # 引用计数从 2 降到 1,不会变为 0
# 引用计数无法回收!需要分代 GC 来处理
分代回收(Generational GC)
Python 使用分代回收来处理循环引用:
| 代 | 存放 | GC 频率 | 触发条件 |
|---|---|---|---|
| 第 0 代 | 新创建的对象 | 最频繁 | 分配数 - 释放数 > 700 |
| 第 1 代 | 存活过 1 次 GC | 较低 | 第 0 代 GC 10 次后 |
| 第 2 代 | 长期存活对象 | 最低 | 第 1 代 GC 10 次后 |
核心算法:标记-清除(Mark and Sweep)
- 从根对象出发,标记所有可达对象
- 清除所有不可达对象(包括循环引用的"孤岛")
gc 模块
import gc
# 查看阈值
print(gc.get_threshold()) # (700, 10, 10)
# 手动触发 GC
gc.collect()
# 查看各代对象数量
print(gc.get_count()) # (45, 3, 1)
# 禁用/启用 GC(高性能场景)
gc.disable()
# ... 批量创建对象 ...
gc.enable()
gc.collect()
# 查找循环引用
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print(gc.garbage) # 不可回收的循环引用对象
weakref 弱引用
弱引用不增加引用计数,不阻止对象被回收:
import weakref
class Cache:
def __init__(self, data):
self.data = data
obj = Cache("重要数据")
weak = weakref.ref(obj) # 创建弱引用
print(weak()) # <Cache object>(对象存活)
del obj
print(weak()) # None(对象已被回收)
# WeakValueDictionary:值是弱引用的字典
cache = weakref.WeakValueDictionary()
obj = Cache("数据")
cache["key"] = obj
del obj # cache["key"] 自动消失
常见面试问题
Q1: Python 的垃圾回收机制?
答案:
Python 使用 引用计数 + 分代回收 双重机制:
- 引用计数:主要机制,实时回收。引用计数为 0 时立即释放
- 分代回收:辅助机制,处理循环引用。将对象分为 3 代,使用标记-清除算法
Q2: 什么时候会出现内存泄漏?
答案:
Python 中的"内存泄漏"通常是意外的引用导致对象无法被回收:
- 全局变量/类变量持有引用
- 闭包捕获大对象
- 循环引用中有
__del__方法(Python 3.4 之前无法回收) - C 扩展中的引用错误
- 缓存无限增长
# 常见泄漏:闭包捕获大对象
def create_handler():
huge_data = [0] * 10_000_000 # 大列表
def handler():
return len(huge_data) # 闭包引用,huge_data 无法释放
return handler
Q3: __del__ 析构方法有什么问题?
答案:
- 调用时机不确定(依赖 GC)
- 循环引用中有
__del__可能导致回收顺序问题 - 异常在
__del__中会被忽略 - 推荐用上下文管理器替代资源清理