第1章:堆内存详解:堆与自由存储区、malloc/free底层原理、堆分配策略与内存碎片问题
大家好,我是你们的老朋友。今天咱们来聊聊堆内存——这个让无数C++程序员又爱又恨的东西。
说实话,我刚开始学C++那会儿,对堆的理解就是「new出来的东西放堆上」。后来踩了不少坑,才真正搞明白它到底是怎么回事。嗯,今天就把这些经验掰开揉碎了讲给你听。
1.1 堆与自由存储区:它们到底是不是一回事?
很多人以为「堆」和「自由存储区」是同一个概念。其实不然。
堆(Heap)是操作系统提供的一块内存区域,由malloc/free管理。自由存储区(Free Store)是C++中new/delete操作的对象。两者在大多数实现中底层是同一块内存,但概念上不同。
关键区别:
malloc/free是C标准库函数,操作的是堆new/delete是C++运算符,操作的是自由存储区new可以调用构造函数,malloc不行delete会调用析构函数,free不会
我在项目中遇到过一个问题:用malloc分配了一个对象,然后直接当C++对象用。结果析构函数没被调用,资源泄漏了。那叫一个惨。
我的建议:C++代码里尽量用new/delete,别混用。除非你在写底层内存池,否则malloc能不用就不用。
1.2 malloc/free底层原理:它到底怎么工作的?
你写一句malloc(100),背后发生了什么?我带你走一遍。
首先,malloc不是直接向操作系统要内存。那样太慢了。它维护了一个内存池,先向OS申请一大块(比如通过brk或mmap),然后自己慢慢分。
具体流程是这样的:
- 检查空闲链表:有没有大小合适的空闲块?
- 如果有:分割或直接返回
- 如果没有:向OS申请新的内存页
- 返回指针:指向分配区域的开头
每个分配的内存块前面,其实藏着一个「头部」结构。里面记录了块的大小、是否空闲等信息。这就是为什么free的时候只需要传一个指针——它从指针往前偏移就能找到头部。
// 简化版的内存块头部结构
struct mem_block {
size_t size; // 块大小(包括头部)
int free; // 是否空闲
struct mem_block *next; // 指向下一个块
};
注意:千万不要越界写!你写多了几个字节,可能就把下一个块的头部给覆盖了。到时候free的时候程序直接崩。我曾经调试过一个这样的bug,花了整整两天。
1.3 堆分配策略:first-fit vs best-fit
堆管理器怎么从空闲块里挑一个给你?主要有两种策略。
First-Fit(首次适应)
从头遍历空闲链表,找到第一个大小够用的块就返回。
- 优点:速度快,简单粗暴
- 缺点:容易产生碎片,尤其是前面有很多小块时
Best-Fit(最佳适应)
遍历所有空闲块,找到大小最接近你需求的块。
- 优点:浪费少,碎片相对少一些
- 缺点:慢,每次都要遍历全部
我个人习惯在嵌入式系统里用first-fit,因为快。在服务器端程序里用best-fit,因为内存更宝贵。你想想看,一个高频交易系统,内存碎片多了,延迟就上去了,那损失可大了。
实际对比:
| 策略 | 分配速度 | 内存利用率 | 碎片程度 |
|---|---|---|---|
| First-Fit | 快 | 中等 | 高 |
| Best-Fit | 慢 | 高 | 低 |
1.4 内存碎片问题:看不见的杀手
内存碎片分两种:内部碎片和外部碎片。
内部碎片:你申请了100字节,但分配器给了你128字节的块。多出来的28字节就是内部碎片。说白了,就是浪费在你「块内部」的空间。
外部碎片:一堆小空闲块散布在堆里,每个都不够大,但加起来很大。你申请一个大块时,明明总空闲内存够,但就是分配不出来。
我曾经在一个网络服务器项目里遇到过这个问题。跑了三天,内存占用才50%,但突然就malloc失败了。一查,堆里全是4字节、8字节的小碎片。这就是典型的外部碎片。
避坑指南:
- 避免频繁分配和释放不同大小的对象
- 尽量使用对象池或内存池
- 大对象用
mmap单独映射,不和堆混在一起 - 定期检查碎片率,可以用
mallinfo函数
1.5 知识体系总览
下面这张图,是我自己画的。它把本章的核心逻辑串起来了。你看一遍,心里就有谱了。
这张图把堆、分配策略、碎片问题串成了一条线。你从左边看到右边,再从上看到下,整个知识脉络就清晰了。
1.6 小结
堆内存管理,说白了就是「怎么高效地分内存、还内存」。别小看它。我见过太多线上事故,都是堆碎片搞的鬼。
记住三点:
- 分清堆和自由存储区,别混用
malloc和delete - 理解
malloc底层有头部,别越界 - 碎片是慢性病,早预防比晚治疗强
下一章咱们聊new/delete的底层实现,以及placement new的妙用。到时候见。