1105 字
3 分钟
引用计数(Reference Counting)
一、为什么需要引用计数
一个视频编辑器打开了多个素材文件,不同时间线轨道引用同一个素材。最后一个轨道释放这个素材时,文件句柄才能关闭。问题是:你怎么知道”最后一个”是什么时候?
垃圾回收器可以帮你回收内存,但它不保证回收时机——GC 什么时候跑是运行时决定的。文件句柄、数据库连接、GPU 缓冲区这些资源不能等 GC 心情好再释放。你需要的是确定性的:最后一个引用消失的瞬间,资源立刻清理。
引用计数给出的答案是:给每个共享资源配一个计数器,新引用加一,引用释放减一,归零时立即清理。没有 GC 停顿,没有终结器队列,完全确定性。
二、现实类比
合租的 Netflix 账号。你记录有多少人在用。最后一个人退出时,订阅自动取消。不需要后台定时检查——计数归零本身就是信号。
三、核心思想
引用计数为每个共享资源分配一个计数器。每个新所有者(clone)使计数加一;每次释放(drop)使计数减一。当计数归零时,资源立即被清理。
flowchart TD
A["创建资源\nrefcount = 1\nowner: A"] --> B["A.clone() → B\nrefcount = 2"]
B --> C["A.drop()\nrefcount = 1"]
C --> D["B.drop()\nrefcount = 0"]
D --> E["cleanup()!"]
text
┌────────────┐
│ Resource │ refcount = 1
│ (value) │
└─────┬──────┘
│
owner A
A.clone() → B
┌────────────┐
│ Resource │ refcount = 2
│ (value) │
└──┬─────┬───┘
│ │
owner A owner B
A.drop()
┌────────────┐
│ Resource │ refcount = 1
│ (value) │
└─────┬──────┘
│
owner B
B.drop() → refcount = 0 → cleanup()!
| 属性 | 值 | 说明 |
|---|---|---|
| Clone | O(1) | 计数器加一 |
| Drop | O(1) | 计数器减一,条件性清理 |
| 清理触发 | 确定性 | 最后一个所有者 drop 时立即触发 |
| 线程安全 | 需要原子操作 | 多线程使用需要原子计数器或互斥锁 |
引用计数的致命弱点:循环引用。对象 A 引用 B,B 又引用 A,两者的计数永远不会归零,造成内存泄漏。CPython 用循环收集器作为补充,Rust 用 Weak<T> 打破循环。
四、变体与对比
| 模式 | 与引用计数的关系 | 适用场景 |
|---|---|---|
| 写时复制(CoW) | 引用计数决定何时需要复制 CoW 值 | 读多写少的共享数据 |
| 对象池 | 池提供替代方案——归还对象而不是释放 | 高频创建销毁的短期对象 |
| 墓碑 | 墓碑延迟清理,引用计数延迟释放 | LSM 树和分布式存储 |
| Arena 分配器 | Arena 通过作用域结束释放避免逐对象引用计数 | 生命周期统一的大量对象 |
| 追踪式 GC | GC 通过可达性分析解决循环引用问题 | 对象关系复杂的通用场景 |
五、多语言实现
5.1 Go 实现
package refcount
import "sync"
// RefCounted 引用计数的共享资源type RefCounted[T any] struct { mu sync.Mutex value T count int cleanup func(T)}
// NewRefCounted 创建一个引用计数的资源func NewRefCounted[T any](value T, cleanup func(T)) *RefCounted[T] { return &RefCounted[T]{ value: value, count: 1, cleanup: cleanup, }}
// Clone 增加引用计数,返回同一指针func (rc *RefCounted[T]) Clone() *RefCounted[T] { rc.mu.Lock() defer rc.mu.Unlock() rc.count++ return rc}
// Drop 减少引用计数,归零时触发清理func (rc *RefCounted[T]) Drop() { rc.mu.Lock() defer rc.mu.Unlock() rc.count-- if rc.count == 0 && rc.cleanup != nil { rc.cleanup(rc.value) }}
// Count 返回当前引用计数func (rc *RefCounted[T]) Count() int { rc.mu.Lock() defer rc.mu.Unlock() return rc.count}使用示例——文件句柄管理:
func processFile(path string) { f, _ := os.Open(path) // 创建引用计数的文件句柄 handle := refcount.NewRefCounted(f, func(file *os.File) { file.Close() // 最后一个引用释放时关闭文件 log.Println("file closed:", path) })
// 传递给多个处理函数 h1 := handle.Clone() h2 := handle.Clone()
go func() { defer h1.Drop() // 读取文件... }()
go func() { defer h2.Drop() // 读取文件... }()
handle.Drop() // 主引用释放 // 文件在最后一个 Drop() 时才真正关闭}5.2 TypeScript 实现
type CleanupFn<T> = (value: T) => void;
interface RefCountedInner<T> { value: T; count: number; dropped: boolean; cleanup: CleanupFn<T>;}
class RefCounted<T> { private inner: RefCountedInner<T>; private owned: boolean;
constructor(value: T, cleanup: CleanupFn<T>) { this.inner = { value, count: 1, dropped: false, cleanup }; this.owned = true; }
// 创建新的共享引用 clone(): RefCounted<T> { if (!this.owned) throw new Error("Cannot clone a dropped reference"); this.inner.count++; const cloned = Object.create(RefCounted.prototype) as RefCounted<T>; cloned.inner = this.inner; cloned.owned = true; return cloned; }
// 释放引用,归零时触发清理 drop(): void { if (!this.owned) return; // 双重 drop 是空操作 this.owned = false; this.inner.count--; if (this.inner.count === 0 && !this.inner.dropped) { this.inner.dropped = true; this.inner.cleanup(this.inner.value); } }
refCount(): number { return this.inner.count; }
value(): T { if (!this.owned) throw new Error("Reference has been dropped"); return this.inner.value; }}六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| CPython | refcount.h#L255-L310 | Py_INCREF / Py_DECREF 是 CPython 的主要内存管理机制。每个 Python 对象携带 ob_refcnt,归零时调用 _Py_Dealloc。GC 仅用于打破引用循环 |
| Rust std | sync.rs#L269-L276 | Arc<T> 原子引用计数,Drop 时 fetch_sub(1, Release),归零时调用 drop_slow()。在 Tokio、Actix 中广泛使用 |
| Swift ARC | Swift 编译器 | Swift 的整个内存模型基于自动引用计数,编译器自动插入 retain/release 调用 |
七、小结
何时使用:
- 需要确定性清理的共享所有权——文件句柄、GPU 缓冲区、数据库连接
- 避免垃圾回收停顿——实时系统(游戏、音频)中无法接受 stop-the-world
- 跨语言互操作——CPython 引用计数让 C 扩展自然管理 Python 对象
- 短期共享状态——对象主要由一处拥有但偶尔短暂共享
何时不用:
- 循环数据结构——双向链表、图节点会导致计数永远不归零,用弱引用或追踪式 GC
- 高竞争共享——多线程频繁 clone/drop 同一对象,原子计数器成为缓存行瓶颈
- 批量分配模式——大量小对象逐个计数开销大,用 Arena 分配替代
八、参考资料
- CPython 引用计数 - CPython 核心内存管理机制
- Rust Arc 源码 - Rust 标准库原子引用计数实现
- Swift ARC 文档 - Swift 自动引用计数的设计与用法
- Linux kernel kref - Linux 内核对象的引用计数基础设施
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
引用计数(Reference Counting)
https://blog.souloss.com/posts/programming/memory/memory-reference-counting/ 部分信息可能已经过时
相关文章 智能推荐
1
弱引用与引用强度(Weak / Soft / Phantom Reference)
程序设计 不同强度的可达性,配合 GC 实现缓存和资源清理——从强引用到虚引用的完整谱系
2
分代垃圾回收(Generational GC)
程序设计 对象分代,年轻代频繁回收——JVM、V8、Go 运行时的核心内存管理策略
3
Arena 分配器(Arena Allocator)
程序设计 整块申请内存,用完一次性释放——bump pointer 分配的极致性能,编译器和游戏引擎的核心内存策略。
4
空闲链表(Free List)
程序设计 用链表管理已释放的内存块,O(1) 分配和释放——操作系统内核和游戏引擎的底层分配利器。
5
享元(Flyweight)
程序设计 共享不可变的部分,只存储变化的部分——用更少的对象表示更多的数据,游戏引擎和编译器中的经典内存优化。






