4、内存碎片问题:内部碎片与外部碎片
内存碎片,说白了就是内存被切得七零八落,想用却用不了。我做了这么多年嵌入式,见过太多系统因为碎片问题莫名其妙挂掉。你想想看,明明总内存还剩不少,但就是分配不出一块连续的大内存——这就是碎片的典型症状。
碎片分两种:内部碎片和外部碎片。咱们一个一个说清楚。
4.1 内部碎片
内部碎片,就是分配出去的内存块里,没用上的那部分。举个例子:你申请了10个字节,但分配器给你一个16字节的块。多出来的6个字节,就是内部碎片。
为什么会这样?因为内存分配器通常按固定大小管理内存。比如我常用的伙伴系统,最小块是32字节。你申请1个字节,它也给你32字节。剩下的31个字节,全成了内部碎片。
内部碎片的本质:分配粒度大于实际需求,造成的浪费。
我在一个物联网项目里遇到过这种情况。设备只有64KB RAM,任务栈每个都申请了512字节。但实际运行时,大部分任务只用不到200字节。你算算,每个任务浪费300多字节,10个任务就是3KB。在资源受限的系统里,这可不是小数目。
4.2 外部碎片
外部碎片更隐蔽,也更难搞。它指的是:空闲内存总量够,但分散在不同地方,无法满足一次连续分配。
想象一下这个场景:内存从地址0x1000到0x2000,总共4KB。你分配了A(1KB)、B(1KB)、C(1KB),然后释放了B。现在空闲块有两个:0x1000-0x13FF(1KB)和0x1800-0x1BFF(1KB)。这时候你想分配1.5KB的连续内存——对不起,做不到。虽然总空闲有2KB,但都是碎片。
我的经验:外部碎片在长时间运行的系统中特别致命。服务器跑几个月,碎片越积越多,最后连个小内存都分配不出来。
4.3 碎片产生的根本原因
碎片不是凭空冒出来的。根因就两个:
- 分配和释放的顺序不匹配:你申请的顺序是A、B、C,释放的顺序是B、A、C。这种交叉释放,必然产生空洞。
- 分配大小不固定:每次申请的内存大小都不一样。今天申请32字节,明天申请128字节,后天申请64字节。大小参差不齐,很难拼凑出连续大块。
说白了,碎片就是「时间上的不连续」和「空间上的不连续」叠加的结果。你想想看,如果所有任务都按固定大小申请内存,释放时也按固定顺序,碎片问题基本不存在。但现实世界哪有这么理想?
注意:有些新手以为内存越大越不容易碎片。这是误解。碎片率跟内存总量无关,跟分配释放的模式有关。1GB内存的系统,如果分配模式糟糕,照样碎片满天飞。
4.4 碎片对系统的影响
碎片不是理论问题,它会实实在在地搞垮你的系统。我总结了几种典型影响:
| 影响类型 | 具体表现 | 严重程度 |
|---|---|---|
| 分配失败 | 明明有足够空闲内存,但分配大块时失败 | 致命 |
| 性能下降 | 分配器需要遍历空闲链表,查找合适块 | 中等 |
| 内存利用率低 | 实际可用内存远小于总量 | 严重 |
| 系统不稳定 | 长时间运行后,分配失败概率越来越高 | 致命 |
我曾经维护过一个工业控制器的代码。设备跑了两三个月后,突然报内存分配失败。查了半天,发现是外部碎片导致的。系统总内存256KB,空闲内存还有80KB,但最大连续块只有12KB。而某个任务需要申请16KB的缓冲区——直接挂了。
嗯,这里要注意:碎片对实时系统的影响尤其大。实时系统要求分配时间可预测,但碎片严重时,分配器可能要遍历几百个空闲块才能找到合适的。这个时间不可控,可能导致任务错过截止时间。
4.5 如何衡量碎片程度
做内核开发,不能光凭感觉说「碎片严重」。得有量化指标。我常用的两个指标:
- 碎片率 = 1 - (最大连续空闲块 / 总空闲内存)。越接近1,碎片越严重。
- 分配失败率 = 分配失败次数 / 总分配次数。这个更直观。
举个例子:总空闲内存100KB,最大连续块只有20KB。碎片率就是1 - 20/100 = 0.8。80%的碎片率,系统基本处于危险状态。
我的建议:在系统里加一个监控任务,定期打印碎片率。碎片率超过0.5就报警,超过0.7就触发紧急处理。这个习惯救过我很多次。
4.6 小结
碎片问题,说白了就是内存管理的「癌症」。内部碎片是浪费,外部碎片是阻塞。两者都会降低系统的可靠性和性能。
下一章我会讲怎么解决碎片问题——内存池、伙伴系统、slab分配器,各有各的招。但在此之前,你得先理解碎片是怎么产生的。只有知道病根,才能对症下药。
记住一句话:没有银弹。任何内存管理算法都是在「分配效率」、「内存利用率」、「碎片控制」之间做权衡。没有完美的方案,只有适合你场景的方案。