跳到主要内容

引用计数与分代回收

问题

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)

  1. 从根对象出发,标记所有可达对象
  2. 清除所有不可达对象(包括循环引用的"孤岛")

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 使用 引用计数 + 分代回收 双重机制:

  1. 引用计数:主要机制,实时回收。引用计数为 0 时立即释放
  2. 分代回收:辅助机制,处理循环引用。将对象分为 3 代,使用标记-清除算法

Q2: 什么时候会出现内存泄漏?

答案

Python 中的"内存泄漏"通常是意外的引用导致对象无法被回收:

  1. 全局变量/类变量持有引用
  2. 闭包捕获大对象
  3. 循环引用中有 __del__ 方法(Python 3.4 之前无法回收)
  4. C 扩展中的引用错误
  5. 缓存无限增长
# 常见泄漏:闭包捕获大对象
def create_handler():
huge_data = [0] * 10_000_000 # 大列表
def handler():
return len(huge_data) # 闭包引用,huge_data 无法释放
return handler

Q3: __del__ 析构方法有什么问题?

答案

  • 调用时机不确定(依赖 GC)
  • 循环引用中有 __del__ 可能导致回收顺序问题
  • 异常在 __del__ 中会被忽略
  • 推荐用上下文管理器替代资源清理

相关链接