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 做了两件事:

  1. 调用 operator new 分配内存(底层就是 malloc)
  2. 调用构造函数初始化对象

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 内存池的核心结构

下面这张图,是我画的一个典型内存池结构:

内存池核心结构 内存池大块(一次性申请,例如 64KB) 16B 块 16B 块 32B 块 32B 块 64B 块 空闲链表:16B 块链表 → 32B 块链表 → 64B 块链表 分配时:从对应大小的链表中取一块 释放时:将块放回对应链表(不归还系统)

内存池通常会把大块内存切成固定大小的「槽位」。比如 16 字节、32 字节、64 字节……每个槽位用链表串起来。

分配时,直接从对应大小的链表头部取一块。释放时,把块放回链表头部。整个过程没有系统调用,没有锁竞争(如果设计得好),速度极快。

4.3.2 内存池的典型应用场景

场景 为什么用内存池 我见过的案例
游戏引擎 每帧大量创建/销毁对象 Unity 的 GameObject 池
网络服务器 频繁分配/释放连接对象 Nginx 的内存池
嵌入式系统 内存有限,不能碎片化 FreeRTOS 的 heap 实现

我的建议:如果你在写一个高性能服务,或者内存碎片让你头疼,不妨试试内存池。实现一个简单的固定大小内存池,其实也就几十行代码。但效果立竿见影。

4.4 总结一下

咱们今天聊了三层:

  • malloc/free:C 语言的堆内存接口,底层靠空闲链表和系统调用。性能一般,容易碎片化。
  • new/delete:C++ 的封装,多了构造/析构函数调用。记住 new[] 配 delete[],别混用。
  • 内存池:预分配大块内存,自己管理。速度快、碎片少,适合高频分配场景。

说实话,内存管理这块儿,没有银弹。但理解了底层原理,你至少知道问题出在哪。

下次你写代码时,多问自己一句:「这块内存,真的需要 new 吗?能不能复用?」

嗯,能问出这个问题,你就已经超过 80% 的开发者了。

核心一句话:堆内存不是无限的,也不是免费的。理解它的底层,才能用好它。

可可精品资料库