Python 垃圾回收机制:引用计数、循环引用和分代 GC

很多人第一次关心 Python 的垃圾回收,不是因为写了多复杂的代码,而是因为遇到了几个很实际的问题: del obj 之后,内存为什么没有立刻降下来? 明明没有全局变量引用某个对象,它为什么还活着? Python 不是有垃圾回收吗,为什么还会出现内存泄漏? gc.collect() 到底该不该在业务代码里手动调用? 这些问题背后其实是同一个机制:Python 的内存回收不是单一算法,而是几层机制一起工作。平时我们说的“Python 垃圾回收”,在最常见的 CPython 解释器里,主要由两部分组成: 引用计数:对象引用数变成 0 时,通常立刻释放。 循环垃圾回收器:专门处理引用计数解决不了的循环引用。 这篇文章主要讨论 CPython,因为日常用 python 命令启动的解释器,大多数情况下就是 CPython。PyPy、Jython、IronPython 等实现可以采用不同策略,所以不要把本文所有细节直接套到每一种 Python 实现上。 先从变量和对象的关系说起 在 Python 里,变量不是装对象的盒子,更像是贴在对象上的名字。 a = [1, 2, 3] b = a 这段代码里没有创建两个列表。它创建了一个列表对象,然后让 a 和 b 都指向它: a ─┐ ├──> [1, 2, 3] b ─┘ 执行: del a 删除的也不是列表对象本身,而是删除名字 a 到列表对象的那条引用。因为 b 还指向这个列表,所以对象仍然活着: b ───> [1, 2, 3] 这就是理解 Python 垃圾回收的入口:对象什么时候能被回收,取决于还有没有地方能继续访问它。 del 的含义也要顺手纠正一下:del name 删除的是名字绑定;del obj.attr 删除的是属性引用;del some_list[i] 删除的是容器里的一个引用。它们都不等于“立刻把某块内存还给操作系统”。 ...

June 20, 2026 · 5 min · Icyyan

Rust 里的 Box 到底是什么

学 Rust 时,Box<T> 很容易被一句话带过:它可以把数据放到堆上。 这句话没错,但如果只记住这一句,后面看到 Box<dyn Trait>、Box<List>、Pin<Box<T>>、Box::leak 时,还是会觉得它像一个突然冒出来的语法补丁。 更好的理解方式是:Box<T> 不是“逃离所有权系统”的工具,而是 Rust 所有权系统里最基础的一种拥有型指针。它让一个值住在堆上,同时让所有权仍然清清楚楚地归某个变量管理。 先看 Box 解决了什么问题 普通变量通常可以这样理解: let n = 42; n 这个值本身就放在当前栈帧里。栈很快,进入函数时分配,函数返回时回收,生命周期也很清楚。 但有些场景光靠栈不够舒服: 一个类型递归地包含自己,编译器算不出它的大小。 你想把不同具体类型放进同一个集合,只要求它们实现同一个 trait。 一个值很大,你希望移动时只移动一个指针。 某些 API 需要一个固定地址的拥有型对象,比如配合 Pin 使用。 这时 Box<T> 就出现了。 let n = Box::new(42); 可以把它粗略想成这样: 栈上变量 n -> 保存一个指针 -> 指向堆上的 42 Box<T> 本身在栈上,里面保存指向堆数据的指针。真正的 T 在堆上。变量离开作用域时,Box<T> 会自动释放堆上的 T。 所以 Box<T> 同时有两个特点: 它是指针,可以间接访问堆上的值。 它拥有这个值,离开作用域时负责释放它。 这和 C 里的裸指针很不一样。你不用手写 free,也不能随便复制出多个拥有者。Rust 仍然会检查所有权、移动和借用。 最基本的用法:Box::new 创建一个 Box<T> 最常见的方式是 Box::new: fn main() { let name = Box::new(String::from("cloudside")); println!("{name}"); } 这里 String 这个值由 Box 放到堆上管理,name 是一个拥有它的 Box<String>。注意,String 自己内部还会管理一块字符串缓冲区;这里说的是 String 这个三字段结构本身的位置。 ...

May 24, 2026 · 5 min · Icyyan