4. 堆内存管理
说到内存管理,堆这块儿绝对是重灾区。
我见过太多人,写代码时 new 得飞起,却从来不问一句「这内存到底从哪来的?」。结果呢?线上服务跑着跑着,内存飙上去了,然后 OOM 被 kill 掉。你想想看,这多冤。
今天咱们就把堆内存的底裤扒干净。从 malloc/free 到 new/delete,再到内存池,一层层拆开看。
4.1 malloc/free:C 语言的堆内存基石
先聊聊 malloc。这玩意儿是 C 标准库提供的,原型很简单:
void* malloc(size_t size);
void free(void* ptr);
但它的底层,其实挺复杂。
4.1.1 malloc 是怎么分配内存的?
说白了,malloc 内部会维护一个「空闲链表」。你申请内存时,它就去链表里找一块足够大的空闲块给你。
如果找不到呢?它会调用系统调用 brk() 或 mmap(),向操作系统要一块新的内存页。
关键点:malloc 分配的内存,不一定来自物理内存。它只是从虚拟地址空间里划了一块给你。真正用到物理内存,是在你第一次读写这块内存的时候——这叫「缺页中断」。
我记得有一次排查内存泄漏,发现程序 RSS 一直涨,但 malloc 的次数并不多。后来才发现,是频繁读写导致缺页中断,物理页被不断分配。嗯,这里要注意:虚拟内存 ≠ 物理内存。
4.1.2 free 做了什么?
free 不会把内存还给操作系统。它只是把这块内存标记为「空闲」,放回空闲链表。
这样做的好处是:下次 malloc 可以直接复用,不用再走系统调用,速度快很多。
坏处呢?你 free 了,但进程的虚拟内存空间并不会缩小。这就是为什么有些程序 free 完内存,RSS 还是居高不下。
避坑指南:我曾经在写一个长连接服务时,频繁 malloc/free 小块内存,结果内存碎片化严重。明明总空闲内存够用,但 malloc 就是分配不出连续的大块内存。后来改用内存池,问题才解决。
4.2 new/delete:C++ 的封装与扩展
new/delete 是 C++ 的关键字,不是函数。它们底层会调用 malloc/free,但多了些东西。
4.2.1 new 的完整流程
当你写 int* p = new int(42); 时,编译器会生成类似这样的代码:
// 伪代码
void* raw_memory = operator new(sizeof(int)); // 分配原始内存
int* p = static_cast<int*>(raw_memory);
p->int::int(42); // 调用构造函数
看到了吗?new 做了两件事:
- 调用
operator new分配内存(底层就是 malloc) - 调用构造函数初始化对象
delete 则反过来:先调用析构函数,再调用 operator delete(底层就是 free)。
个人习惯:我建议你在 C++ 项目中,能用智能指针就别裸写 new/delete。但如果你非要写,请一定成对出现。我见过太多「new 了但没 delete」的惨案。
4.2.2 new[] 和 delete[] 的陷阱
这个坑我踩过。你写 int* arr = new int[10]; 时,编译器会在数组头部多分配 4 或 8 个字节,用来记录数组长度。
然后你 delete 时,必须用 delete[] arr;。否则,编译器不知道要调用多少次析构函数,也不知道要释放多少内存。
我曾经:用 delete 代替 delete[] 去释放一个对象数组,结果只调用了第一个元素的析构函数,后面 9 个对象的内存泄漏了。查了一下午才找到原因。嗯,从那以后我再也不敢混用 delete 和 delete[] 了。
4.3 内存池概念:为什么需要它?
聊完了 malloc 和 new,咱们说说内存池。
你想想看,如果每次分配内存都走 malloc,频繁调用系统调用,性能能好吗?而且 malloc 分配的内存地址不连续,缓存命中率也低。
内存池的思路很简单:一次性向系统申请一大块内存,然后自己管理这块内存的分配和释放。
4.3.1 内存池的核心结构
下面这张图,是我画的一个典型内存池结构:
内存池通常会把大块内存切成固定大小的「槽位」。比如 16 字节、32 字节、64 字节……每个槽位用链表串起来。
分配时,直接从对应大小的链表头部取一块。释放时,把块放回链表头部。整个过程没有系统调用,没有锁竞争(如果设计得好),速度极快。
4.3.2 内存池的典型应用场景
| 场景 | 为什么用内存池 | 我见过的案例 |
|---|---|---|
| 游戏引擎 | 每帧大量创建/销毁对象 | Unity 的 GameObject 池 |
| 网络服务器 | 频繁分配/释放连接对象 | Nginx 的内存池 |
| 嵌入式系统 | 内存有限,不能碎片化 | FreeRTOS 的 heap 实现 |
我的建议:如果你在写一个高性能服务,或者内存碎片让你头疼,不妨试试内存池。实现一个简单的固定大小内存池,其实也就几十行代码。但效果立竿见影。
4.4 总结一下
咱们今天聊了三层:
- malloc/free:C 语言的堆内存接口,底层靠空闲链表和系统调用。性能一般,容易碎片化。
- new/delete:C++ 的封装,多了构造/析构函数调用。记住 new[] 配 delete[],别混用。
- 内存池:预分配大块内存,自己管理。速度快、碎片少,适合高频分配场景。
说实话,内存管理这块儿,没有银弹。但理解了底层原理,你至少知道问题出在哪。
下次你写代码时,多问自己一句:「这块内存,真的需要 new 吗?能不能复用?」
嗯,能问出这个问题,你就已经超过 80% 的开发者了。
核心一句话:堆内存不是无限的,也不是免费的。理解它的底层,才能用好它。
可可精品资料库