CPython(标准的Python解释器实现)的内存管理机制是高效且巧妙的。它主要依赖引用计数(Reference Counting),但为了弥补引用计数无法处理的缺陷(即循环引用),CPython引入了分代垃圾回收(Generational Garbage Collection)。
1. 引用计数:CPython的基石
引用计数是 CPython 最基础、最主要的垃圾回收方式。每个Python对象内部都有一个计数器,记录着有多少个引用指向它。当引用计数变为零时,对象立即被释放。
优点: 简单高效,内存释放实时性高(没有明显的暂停)。
缺点: 无法处理循环引用,且维护计数器本身需要额外的开销。
我们可以使用 sys.getrefcount() 来查看一个对象的当前引用计数(注意,调用此函数本身会临时增加一次引用)。
import sys
a = []
print(f"初始引用计数: {sys.getrefcount(a) - 1}") # 结果通常为1
b = a # 增加一个引用
c = a # 再增加一个引用
print(f"新的引用计数: {sys.getrefcount(a) - 1}") # 结果通常为3
del b
print(f"删除一个引用后的计数: {sys.getrefcount(a) - 1}") # 结果通常为2
2. 循环引用:引用计数的盲区
当两个或多个对象相互引用,形成一个闭环,即使外部对它们的引用全部消失,它们的引用计数也不会降为零。这会导致它们永远无法被引用计数机制回收,造成内存泄漏。
class Node:
def __init__(self, name):
self.name = name
self.neighbor = None
# 创建一个循环引用
def create_cycle():
a = Node("A")
b = Node("B")
# 形成循环:A引用B,B引用A
a.neighbor = b
b.neighbor = a
# 此时,a和b的引用计数都至少为2(内部相互引用 + 外部在函数栈帧中的引用)
# 函数结束后,外部引用消失,但相互引用仍然存在!
print("循环引用对象已创建,外部引用即将消失...")
import gc
# 清空旧的垃圾(确保实验环境干净)
gc.collect()
print(f"回收前对象数量: {len(gc.get_objects())}")
create_cycle()
# 此时,a和b对象在外部已经不可达,但它们仍然占据内存。
# 只有当GC循环检测器运行时,它们才会被清除。
gc.collect()
print(f"回收后对象数量: {len(gc.get_objects())}")
# 理论上,gc.collect() 运行后,循环对象被成功清理。
3. 分代回收机制(Generational GC)
为了处理循环引用并优化性能,CPython 引入了分代回收机制,其基于“弱代假设”:大多数对象寿命很短,少数对象寿命很长。
CPython将对象划分为三个代(Generation):
| 代(Generation) | 触发阈值 | 特点 |
|---|---|---|
| Gen 0 | 新生代 | 存放新创建的对象。GC最频繁。 |
| Gen 1 | 中年代 | 经过一次Gen 0回收后存活下来的对象。GC频率较低。 |
| Gen 2 | 老年代 | 经过一次Gen 1回收后存活下来的对象。GC频率最低。 |
分代回收策略只在达到一定阈值(Threshold)时触发,并且只会针对那些无法被引用计数回收的对象(主要是容器对象如列表、字典、实例对象等)。
3.1 循环检测器的工作原理
当某一世代的回收被触发时,GC会执行以下步骤来检测并清理循环引用:
- 标记不可达对象: 对于待检测的对象集合,GC会暂时性地降低对象的引用计数,模拟它们外部引用消失的情况。
- 追踪: GC从根集开始遍历,标记那些通过有效路径(即引用计数大于0)仍然可达的对象。
- 识别垃圾: 那些在降低引用计数后,最终被判定为不可达的对象(即它们只能通过彼此形成循环来保持计数),被标记为垃圾。
- 清理: 销毁这些被标记的垃圾对象,并安全地移除它们之间的引用。
3.2 控制和查看 GC 状态
我们可以使用 gc 模块来手动控制和查看分代回收的状态。
import gc
# 查看当前的GC阈值 (默认通常是 700, 10, 10)
print(f"GC阈值 (Gen 0, Gen 1, Gen 2): {gc.get_threshold()}")
# 解释阈值:
# 当 Gen 0 中新增的对象数量 减去 被回收的对象数量 达到 700 时,触发 Gen 0 回收。
# 如果 Gen 0 触发了 N 次回收,其中有 10 次存活的对象被提升到 Gen 1,则触发 Gen 1 回收。
# Gen 2 的触发机制类似。
# 手动触发一次全局回收
collect_count = gc.collect()
print(f"手动触发回收,本次清理的对象数量: {collect_count}")
# 设置更激进的阈值(例如,每创建100个对象就尝试回收)
# gc.set_threshold(100, 3, 3)
# print(f"新的GC阈值: {gc.get_threshold()}")
总结: CPython 的高效内存管理是引用计数(快而频繁)和分代循环检测(慢而周期性)协同工作的成果。引用计数处理了绝大多数对象的生命周期,而分代回收则专门解决了棘手的循环引用问题,并根据对象的年龄来优化检测频率,最大限度地减少了程序暂停时间。
汤不热吧