mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1105 字
3 分钟
引用计数(Reference Counting)
2026-06-13

一、为什么需要引用计数#

一个视频编辑器打开了多个素材文件,不同时间线轨道引用同一个素材。最后一个轨道释放这个素材时,文件句柄才能关闭。问题是:你怎么知道”最后一个”是什么时候?

垃圾回收器可以帮你回收内存,但它不保证回收时机——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()!
属性说明
CloneO(1)计数器加一
DropO(1)计数器减一,条件性清理
清理触发确定性最后一个所有者 drop 时立即触发
线程安全需要原子操作多线程使用需要原子计数器或互斥锁

引用计数的致命弱点:循环引用。对象 A 引用 B,B 又引用 A,两者的计数永远不会归零,造成内存泄漏。CPython 用循环收集器作为补充,Rust 用 Weak<T> 打破循环。

四、变体与对比#

模式与引用计数的关系适用场景
写时复制(CoW)引用计数决定何时需要复制 CoW 值读多写少的共享数据
对象池池提供替代方案——归还对象而不是释放高频创建销毁的短期对象
墓碑墓碑延迟清理,引用计数延迟释放LSM 树和分布式存储
Arena 分配器Arena 通过作用域结束释放避免逐对象引用计数生命周期统一的大量对象
追踪式 GCGC 通过可达性分析解决循环引用问题对象关系复杂的通用场景

五、多语言实现#

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;
}
}

六、生产验证#

项目源码位置用途
CPythonrefcount.h#L255-L310Py_INCREF / Py_DECREF 是 CPython 的主要内存管理机制。每个 Python 对象携带 ob_refcnt,归零时调用 _Py_Dealloc。GC 仅用于打破引用循环
Rust stdsync.rs#L269-L276Arc<T> 原子引用计数,Drop 时 fetch_sub(1, Release),归零时调用 drop_slow()。在 Tokio、Actix 中广泛使用
Swift ARCSwift 编译器Swift 的整个内存模型基于自动引用计数,编译器自动插入 retain/release 调用

七、小结#

何时使用:

  • 需要确定性清理的共享所有权——文件句柄、GPU 缓冲区、数据库连接
  • 避免垃圾回收停顿——实时系统(游戏、音频)中无法接受 stop-the-world
  • 跨语言互操作——CPython 引用计数让 C 扩展自然管理 Python 对象
  • 短期共享状态——对象主要由一处拥有但偶尔短暂共享

何时不用:

  • 循环数据结构——双向链表、图节点会导致计数永远不归零,用弱引用或追踪式 GC
  • 高竞争共享——多线程频繁 clone/drop 同一对象,原子计数器成为缓存行瓶颈
  • 批量分配模式——大量小对象逐个计数开销大,用 Arena 分配替代

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

引用计数(Reference Counting)
https://blog.souloss.com/posts/programming/memory/memory-reference-counting/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时