专栏文章
专栏文章
GC 垃圾回收系列
1. GC 系列 #01:GC 总览与历史:自动内存管理的起源与演化 2. GC 系列 #02:GC 基础算法:标记-清除、标记-整理、复制收集、引用计数 3. GC 系列 #03:分代假说与分代收集:朝生夕死的工程哲学 4. GC 系列 #04:CMS 收集器:Java 第一个并发收集器的设计与缺陷 5. GC 系列 #05:G1 收集器:Region 化堆与可预测停顿 6. GC 系列 #06:ZGC 与 Shenandoah:亚毫秒级停顿的并发移动收集器 7. GC 系列 #07:Go 的 GC:并发三色标记与混合写屏障 8. GC 系列 #08:Python 的内存管理:引用计数与分代循环 GC 9. GC 系列 #09:GC 调优实践:JVM 日志解读与 G1/ZGC 参数策略

GC 系列 #08:Python 的内存管理:引用计数与分代循环 GC

发布于 2026-05-25 14:16 👁 15 次阅读
#GC#内存管理#理论#Python#CPython

Python(CPython)采用引用计数为主、分代 GC 为辅的混合策略。引用计数提供了即时、确定性的内存回收,分代 GC 专门解决引用计数无法处理的循环引用问题。


目录

章节 说明
引用计数机制 ob_refcnt 与即时回收
循环引用问题 引用计数的致命缺陷
分代循环检测器 gc 模块的工作原理
内存分配器 pymalloc 与对象池
GIL 与 GC 的关系 为什么 CPython 需要 GIL
其他 Python 实现 PyPy、Jython 的 GC 方案

引用计数机制

每个对象都有引用计数

CPython 的每个 Python 对象都在内存中以 PyObject 结构开头:

// CPython 源码:Objects/object.h
typedef struct _object {
    Py_ssize_t ob_refcnt;   // 引用计数
    PyTypeObject *ob_type;  // 类型指针
    // ... 后续是对象实际数据
} PyObject;

引用计数的变化时机

import sys

a = [1, 2, 3]        # 列表对象创建,refcnt = 1
print(sys.getrefcount(a))  # → 2(getrefcount 本身也持有一个引用)

b = a                # refcnt = 2(+1 因为 b 引用它)
c = a                # refcnt = 3

del b                # refcnt = 2(-1)
del c                # refcnt = 1(-1)

def func(x):
    pass             # 函数调用时 refcnt 临时 +1,返回后 -1
func(a)

del a                # refcnt = 0 → 立即调用 __del__,释放内存

引用计数变化的场景

操作 效果
变量赋值 a = obj +1
变量赋值覆盖 a = other 原对象 -1
函数参数传入 +1
函数返回 参数 -1
放入容器 list.append(obj) +1
从容器删除 list.remove(obj) -1
del a -1
对象超出作用域 -1

引用计数的优势

# with 语句利用了引用计数的确定性
with open("file.txt") as f:
    data = f.read()
# f 超出 with 块,引用计数降为 0,__exit__ 立即被调用,文件立即关闭

循环引用问题

引用计数无法处理循环引用:

# 构造循环引用
class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

a = Node(1)
b = Node(2)
a.next = b    # b 的 refcnt = 2(b 变量 + a.next)
b.next = a    # a 的 refcnt = 2(a 变量 + b.next)

del a         # a 的 refcnt = 1(只剩 b.next 引用它)
del b         # b 的 refcnt = 1(只剩 a.next 引用它)

# 此时:
# - a 和 b 的 refcnt 都是 1,永远不会降为 0
# - 但从 GC Roots 出发,它们完全不可达
# - 内存泄漏!

典型的循环引用场景:


分代循环检测器

Python 的 gc 模块专门用于检测和回收循环引用,使用分代策略:

三代结构

import gc
print(gc.get_threshold())   # → (700, 10, 10)
# 第 0 代:分配 - 释放 > 700 时触发
# 第 1 代:第 0 代 GC 次数 > 10 时触发
# 第 2 代:第 1 代 GC 次数 > 10 时触发

只追踪容器对象

Python GC 只追踪可以包含其他对象的容器(list、dict、set、class 实例等),不追踪 int、str、float 等不可变对象:

import gc
gc.is_tracked([1, 2, 3])    # → True(列表被追踪)
gc.is_tracked((1, 2, 3))    # → False(纯数字元组不追踪)
gc.is_tracked(42)            # → False(整数不追踪)

循环引用检测算法

1. 对所有第 0 代对象,创建引用计数副本(rc_copy)

2. 遍历第 0 代所有对象:
   对于对象 A 引用的每个对象 B:
     rc_copy[B] -= 1   # 减去来自第 0 代内部的引用

3. 减完之后:
   rc_copy > 0 → 还有来自外部(如全局变量)的引用 → 可达,存活
   rc_copy = 0 → 只被第 0 代内部互相引用 → 循环垃圾!

4. 标记为可达的对象晋升到第 1 代
   循环垃圾被回收

内存分配器

pymalloc:小对象池

CPython 使用 pymalloc 管理小于等于 512 字节的对象:

pymalloc 的三级结构:

Arena(256KB)
  └── Pool(4KB,按大小分类)
        └── Block(实际分配单元,8~512 字节)

分配路径:
  请求 24 字节 → 找对应大小 Pool → 从 Pool 取 Block
  比系统 malloc 快很多(减少系统调用,利用缓存)

整数对象池

CPython 预分配了 [-5, 256] 范围内的整数对象:

a = 256
b = 256
print(a is b)   # → True(同一个对象!)

a = 257
b = 257
print(a is b)   # → False(超出预分配范围,不同对象)

# 这就是为什么 Python 中比较整数应该用 == 而不是 is

字符串驻留(String Interning)

a = "hello"
b = "hello"
print(a is b)   # → True(字符串驻留,同一对象)

a = "hello world"
b = "hello world"
print(a is b)   # → 可能 True,也可能 False(取决于编译器优化)

GIL 与 GC 的关系

GIL(Global Interpreter Lock) 是 CPython 的全局解释器锁,每次只允许一个线程执行 Python 字节码。

GIL 与引用计数密切相关:

引用计数是全局状态(每个对象的 ob_refcnt)
多线程同时修改引用计数 → 竞态条件

如果没有 GIL:
  线程 A:读 ob_refcnt = 5
  线程 B:读 ob_refcnt = 5
  线程 A:写 ob_refcnt = 4(减 1)
  线程 B:写 ob_refcnt = 4(也减 1!少减了一次)
  → refcnt 错误 → 内存提前释放 → 悬空指针

GIL 解决:任意时刻只有一个线程修改引用计数
代价:多核 CPU 无法真正并行执行 Python 代码

Python 3.12 免 GIL(PEP 703): 引入每对象锁替代全局 GIL,是 Python 性能的重要里程碑,但需要用原子操作替代引用计数的简单 ++/--


其他 Python 实现

实现 GC 策略 特点
CPython 引用计数 + 分代循环检测 标准实现,即时回收
PyPy 追踪式 GC(类似 JVM) JIT 编译,速度快,GC 停顿略长
Jython JVM GC(G1/ZGC) 运行在 JVM 上,复用 JVM GC
IronPython .NET GC 运行在 .NET 上,复用 CLR GC
MicroPython 标记-清除(嵌入式) 内存极小,专为微控制器设计

PyPy 的例子说明了引用计数不是 Python 的必然选择,而是 CPython 的历史设计决策:

# PyPy 中:
import gc
gc.collect()   # PyPy 用的是追踪式 GC,需要显式触发
# __del__ 的调用时机不确定(取决于 GC 何时运行)

参考资料

  • CPython 源码:Objects/object.cModules/gcmodule.c
  • Pablo Galindo:"Design of CPython's Garbage Collector"
  • PEP 703:"Making the Global Interpreter Lock Optional in CPython"
  • GC 调优实践
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论