8. 内存碎片:内部碎片与外部碎片
内存碎片这个话题,说实话,很多C++开发者写了三五年代码都不一定真正重视过。我早年在一个嵌入式项目里吃过它的亏——程序跑着跑着突然malloc失败,但一看剩余内存还有不少。当时排查了好久,最后发现是碎片把内存搞得支离破碎。嗯,从那以后,我对碎片问题就格外敏感了。
什么是内存碎片?
简单说,内存碎片就是内存被切割成一块块不连续的小区域,导致大块连续内存分配失败。它分两种:内部碎片和外部碎片。你想想看,这就像搬家——内部碎片是你买了个大柜子,里面只放了几件衣服,空间浪费了;外部碎片是客厅里到处散落着小物件,想放个大沙发却找不到一整块地方。
内部碎片(Internal Fragmentation)
内部碎片发生在分配器给你的内存块比你实际需要的要大时。比如你申请了17字节,但分配器按32字节对齐给你,那15字节就白白浪费了。这就是内部碎片。
我在项目中遇到过这样一个场景:一个高频交易系统,每秒要分配释放上百万个小对象。每个对象实际只需要12字节,但默认的malloc会按16字节对齐。看起来只浪费4字节对吧?但乘以百万级,内存开销直接飙升30%以上。
// 内部碎片的典型场景
struct SmallObject {
int id; // 4字节
char type; // 1字节
// 实际需要5字节,但对齐后占8字节
// 3字节内部碎片
};
// 更好的做法:手动控制布局
#pragma pack(push, 1)
struct PackedObject {
int id;
char type;
// 现在只占5字节,但访问速度可能下降
};
#pragma pack(pop)
外部碎片(External Fragmentation)
外部碎片更隐蔽,也更难对付。它指的是空闲内存总量足够,但没有一块连续区域能满足你的分配请求。说白了,就是内存被切成了很多小块,每块都不大,但加起来不少。
为什么会这样?因为你的分配和释放模式不规律。一会儿分配个大的,一会儿释放个中的,久而久之,空闲内存就像瑞士奶酪一样,到处是洞。
// 外部碎片的经典场景
void cause_fragmentation() {
std::vector<void*> blocks;
// 分配大块
for (int i = 0; i < 10; i++) {
blocks.push_back(malloc(1024 * 1024)); // 1MB
}
// 释放奇数索引的块
for (int i = 1; i < blocks.size(); i += 2) {
free(blocks[i]);
blocks[i] = nullptr;
}
// 现在尝试分配一个连续的大块
void* big = malloc(5 * 1024 * 1024); // 5MB
// 很可能失败!虽然空闲总量有5MB,但都是1MB的碎片
}
两种碎片的对比
| 维度 | 内部碎片 | 外部碎片 |
|---|---|---|
| 本质 | 分配块内部浪费 | 空闲块不连续 |
| 主要原因 | 对齐、固定大小池 | 分配释放模式不规则 |
| 检测难度 | 容易(看分配大小) | 较难(需要分析空闲列表) |
| 常见场景 | 小对象大量分配 | 长期运行的服务 |
| 优化手段 | 自定义分配器、紧凑布局 | 内存池、伙伴系统 |
如何检测内存碎片?
我个人习惯用这几个方法:
- 查看/proc/self/smaps(Linux):看Heap区域的连续空闲块大小分布
- 使用Valgrind的massif工具:能画出内存使用的时间线,碎片一目了然
- 写个小工具遍历空闲列表:对于自定义分配器,我经常加一段debug代码,打印所有空闲块的大小和地址
// 一个简单的碎片率计算思路
double fragmentation_ratio(size_t total_free, size_t largest_free_block) {
if (total_free == 0) return 0.0;
// 碎片率 = 1 - (最大连续块 / 总空闲量)
// 越接近1,碎片越严重
return 1.0 - (double)largest_free_block / (double)total_free;
}
实战中的应对策略
说实话,没有银弹。但有几个套路我屡试不爽:
- 对象池:对于频繁创建销毁的同类型对象,用固定大小的对象池。内部碎片可控,外部碎片几乎为零。
- slab分配器:Linux内核的玩法,把内存按2的幂次分成不同大小的slab,分配时找最合适的。我在一个数据库项目里用过,碎片率从30%降到了5%。
- 定期整理:某些场景下,可以暂停服务,把存活对象挪到一起,释放大块连续内存。嗯,这招有点暴力,但管用。
知识体系图
下面这张图帮你理清内存碎片的来龙去脉:
总结一下
内存碎片这东西,说白了就是内存管理中的「隐形杀手」。内部碎片浪费的是空间,外部碎片浪费的是连续性。我个人经验是:先搞清楚你的分配模式,再选对策。如果小对象多,优先考虑对象池或slab分配器;如果分配释放模式复杂,考虑用tcmalloc或jemalloc这类现代分配器,它们内部已经做了大量碎片优化。
嗯,最后提醒一句:别等到线上出问题了才想起内存碎片。写代码时就多想想——这个vector会不会频繁resize?这个list的节点生命周期是怎样的?提前规划,比事后救火强得多。