内存回收策略:延迟释放、垃圾回收、引用计数
内存管理这事儿,说白了就是「借了要还」。你从堆上申请了一块内存,用完了就得还回去。但问题来了——什么时候还?怎么还?谁来盯着你还?
我早年做嵌入式开发时,就吃过内存泄漏的亏。一个跑了几天的设备突然死机,查了半天,发现是一个定时器回调里每次分配了 128 字节,但从来没释放过。嗯,从那以后我对内存回收就特别敏感。
今天咱们聊三种主流的内存回收策略:延迟释放、垃圾回收、引用计数。每种策略都有自己的脾气,咱们一个一个看。
一、延迟释放(Deferred Free)
延迟释放,顾名思义——不急着还。你调用 free() 的时候,系统并没有真的把内存还给操作系统,而是先把它放到一个「待回收池」里,等凑够一批再统一处理。
为什么要这么做?因为 free() 本身是有开销的。频繁地申请和释放小块内存,会导致性能抖动。我见过一个网络服务器项目,每秒要处理上万个小请求,每个请求都 malloc/free,结果 CPU 时间全耗在内存管理上了。
举个例子,Linux 内核里的 kfree_rcu() 就是一种延迟释放机制。它不立即释放内存,而是等所有读者都离开临界区之后,再安全地回收。
// 伪代码:延迟释放的简单实现
struct deferred_free {
void *ptr;
struct deferred_free *next;
};
static struct deferred_free *pending = NULL;
void my_free(void *ptr) {
struct deferred_free *node = malloc(sizeof(*node));
node->ptr = ptr;
node->next = pending;
pending = node;
}
void flush_deferred() {
while (pending) {
struct deferred_free *node = pending;
pending = node->next;
free(node->ptr);
free(node);
}
}
你看,my_free() 只是把指针存起来,真正的 free() 在 flush_deferred() 里统一执行。这样就能把多次小释放合并成一次大释放。
二、垃圾回收(Garbage Collection)
垃圾回收,简称 GC。你不需要手动 free(),系统会自动帮你找到那些「没人用的内存」,然后回收掉。
C 语言本身没有 GC,但你可以用第三方库,比如 Boehm GC。我曾在某个遗留项目里用过它,说实话,接入成本不高,但性能开销你得心里有数。
GC 的核心逻辑分两步:
- 标记(Mark):从根集合(全局变量、栈变量)出发,遍历所有可达对象,打上标记。
- 清扫(Sweep):遍历整个堆,把没标记的对象回收掉。
说白了,GC 就是「谁在用,谁就活着;没人用,就回收」。听起来很美好,但有个问题——GC 运行时,程序得停下来。这叫 Stop-The-World。你想想看,一个游戏服务器突然卡顿 100 毫秒,玩家会怎么想?
GC 的优点是省心。你不需要记着每个 malloc 都要配一个 free,代码里少了很多配对逻辑。但代价是:你失去了对内存回收时机的精确控制。
三、引用计数(Reference Counting)
引用计数,是另一种思路。每个对象里存一个计数器,记录有多少指针指向它。计数器归零时,立即释放。
这个策略在 C++ 的 shared_ptr 里用得最多。C 语言里你也可以自己实现,比如这样:
struct ref_counted {
int refs;
void *data;
};
void ref_inc(struct ref_counted *obj) {
obj->refs++;
}
void ref_dec(struct ref_counted *obj) {
if (--obj->refs == 0) {
free(obj->data);
free(obj);
}
}
每次赋值、传参、返回时,你都得手动调用 ref_inc 和 ref_dec。这活儿很琐碎,但好处是——内存回收是即时的,没有 GC 那种停顿。
我遇到过这种情况。一个图形渲染管线里,纹理对象和渲染上下文互相引用,结果程序跑着跑着内存就爆了。后来加了个弱引用(weak reference)才解决。
引用计数的另一个问题是性能。每次指针操作都要修改计数器,多线程环境下还得加锁。你想想看,一个高频调用的函数里,每次赋值都多一次原子操作,性能能好到哪去?
三种策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 延迟释放 | 减少系统调用,降低碎片 | 内存占用暂时增大 | 高吞吐、低实时性要求 |
| 垃圾回收 | 自动管理,省心 | 停顿时间不可控 | 桌面应用、脚本引擎 |
| 引用计数 | 即时回收,确定性高 | 循环引用,性能开销大 | 资源管理、智能指针 |
知识体系图
我的建议
这三种策略没有绝对的好坏。我个人的习惯是:
- 嵌入式、实时系统:用延迟释放 + 手动管理,别碰 GC。
- 大型应用、脚本引擎:用 GC,省心,但要做好停顿优化。
- 资源管理、智能指针:用引用计数,但一定要处理好循环引用。
其实很多时候,你会混合使用。比如一个网络库,核心数据用引用计数管理,日志系统用延迟释放,上层业务逻辑用 GC。嗯,这就是现实——没有银弹,只有权衡。
公众号:蓝海资料掘金营,微信deep3321