4. 内存分配策略

说到动态内存分配,很多初学者觉得就是 malloc 和 free 的调用。但真正做过嵌入式底层开发的工程师都知道——分配策略的选择,直接决定了系统的碎片化程度和实时性。

我早年在一个物联网网关项目里,就吃过分配策略的亏。当时用的一个轻量级 RTOS,默认是首次适应算法。跑了一周后,系统突然分配不出内存了,但剩余总量明明还有 30%。查了两天才发现——碎片太多了。从那以后,我对这三种策略的理解就刻在骨子里了。

今天咱们就把这三种经典策略掰开揉碎,讲清楚它们的原理、优缺点,以及我踩过的坑。

4.1 为什么需要分配策略?

先想一个问题:你有一块连续的空闲内存,比如 100KB。现在来了三个请求:A 要 20KB,B 要 30KB,C 要 10KB。分配完再释放 A 和 C,内存就变成了这样:

| 空闲10KB | 已用30KB(B) | 空闲10KB | 空闲50KB |

现在来了个 D 要 15KB。该从哪个空闲块里切?

这就是分配策略要解决的问题——从多个空闲块中,选一个最合适的来满足请求。不同的选择,会导致完全不同的碎片分布和分配效率。

核心目标:在分配速度和内存利用率之间找到平衡点。没有完美的策略,只有最适合场景的策略。

4.2 首次适应算法(First Fit)

这是最直观的策略。说白了就是:从头扫描空闲块链表,找到第一个足够大的块就切。

举个例子:空闲块列表是 [10KB, 20KB, 30KB, 15KB],请求 12KB。首次适应会从 10KB 开始看,不够;看 20KB,够了!就从 20KB 里切出 12KB,剩下 8KB 作为新空闲块。

优点

  • 速度快:不需要遍历所有空闲块,找到第一个就停。平均情况下,只需要扫描一半的链表。
  • 实现简单:一个单向链表 + 一个指针遍历,代码量很少。
  • 对大块友好:倾向于保留地址空间尾部的大块,适合大内存请求。

缺点

  • 碎片化严重:容易在内存低地址区域产生大量小碎片。我那个物联网项目就是这个问题——低地址全是几字节的小洞,高地址的大块被保留着,但中间的小请求根本用不上。
  • 扫描开销不稳定:最坏情况下要遍历整个链表,实时性难以保证。

我的经验:在实时性要求不高的嵌入式系统里,首次适应是首选。因为它简单、可靠。但如果你要跑长期任务(比如几个月不重启),建议配合内存整理机制,或者干脆换策略。

4.3 最佳适应算法(Best Fit)

这个策略的名字有点误导人。它并不是「最佳」,而是「最不浪费」。原理是:遍历所有空闲块,找到大小最接近请求的那个。

还是上面的例子:空闲块 [10KB, 20KB, 30KB, 15KB],请求 12KB。最佳适应会选 15KB 那个块,因为 15 - 12 = 3KB,浪费最少。

优点

  • 外部碎片最小化:每次分配都尽量「严丝合缝」,产生的剩余碎片很小。
  • 大块保留得好:不会轻易切大块,大块留给后续的大请求。

缺点

  • 速度慢:必须遍历所有空闲块才能找到最佳匹配。链表越长,耗时越线性增长。
  • 产生大量微小碎片:每次剩个 3 字节、5 字节,这些小碎片几乎无法再利用。我见过一个系统,最佳适应跑了三天,空闲块数量从 20 个暴涨到 200 多个,全是几字节的「垃圾」。
  • 实现复杂:需要维护有序链表(按大小排序),或者每次遍历比较。

避坑指南:我曾经在一个内存只有 64KB 的 MCU 上尝试最佳适应,结果分配器本身的管理开销就占了 2KB。对于小内存系统,最佳适应往往得不偿失——它节省的那点碎片,还不够管理结构占的空间。

4.4 最差适应算法(Worst Fit)

这个策略和最佳适应正好相反:选最大的空闲块来分配。目的是让剩余的空闲块尽可能大,避免产生小碎片。

例子:空闲块 [10KB, 20KB, 30KB, 15KB],请求 12KB。最差适应会选 30KB 那个块,切完后剩下 18KB,仍然是一个较大的空闲块。

优点

  • 剩余块大:每次分配后留下的空闲块仍然比较大,后续分配更容易找到合适空间。
  • 碎片相对均匀:不会像首次适应那样在低地址堆积小碎片。

缺点

  • 浪费严重:总是切最大的块,导致大块被快速消耗殆尽。一旦有大请求到来,可能就找不到合适的块了。
  • 性能差:同样需要遍历所有空闲块找到最大的那个。
  • 实际效果不稳定:很多研究论文表明,最差适应在大多数场景下表现不如首次适应。我个人很少用它。

一句话总结:最差适应试图「以空间换时间」,但实际效果往往两头不讨好——既没省时间,又浪费了空间。

4.5 三种策略对比

策略 分配速度 内存利用率 碎片程度 实现复杂度 推荐场景
首次适应 快(平均) 中等 高(低地址) 低 通用嵌入式系统
最佳适应 慢 高(理论) 高(微小碎片) 高 内存充足、请求大小固定的场景
最差适应 慢 低 中等 高 极少使用

4.6 我的选择建议

做了这么多年嵌入式,我个人的经验是:

  • 80% 的场景用首次适应。它简单、可靠,配合伙伴系统(Buddy System)或 slab 分配器,能覆盖绝大多数需求。
  • 15% 的场景用最佳适应。比如你明确知道请求大小是固定的(像网络协议栈的 mbuf),或者内存足够大、碎片不是问题。
  • 5% 的场景用最差适应。说实话,我只有在做学术对比测试时才用过它。生产环境里几乎没见过。

一个小技巧:如果你不确定选哪个,可以先跑一个内存碎片模拟器。把实际的分配/释放序列录下来,回放测试三种策略。我当年就是这么干的——跑了一晚上数据,发现首次适应最适合我的项目。

4.7 知识体系图

下面这张图帮你理清三种策略的核心逻辑:

内存分配策略对比 首次适应 First Fit 最佳适应 Best Fit 最差适应 Worst Fit ✅ 速度快 ✅ 实现简单 ⚠ 低地址碎片 ⚠ 扫描不稳定 ✅ 浪费最少 ✅ 保留大块 ⚠ 速度慢 ⚠ 微小碎片多 ✅ 剩余块大 ✅ 碎片均匀 ⚠ 浪费大块 ⚠ 性能差 核心权衡 分配速度 vs 内存利用率 vs 碎片程度 没有银弹,根据场景选择

嗯,看到这里你应该明白了——这三种策略没有绝对的好坏。关键是要理解你的应用场景:请求大小分布、分配频率、实时性要求、内存总量,这些因素共同决定了该选哪个。

我个人的习惯是:先用首次适应跑起来,如果发现碎片问题严重,再考虑换成最佳适应或者引入伙伴系统。别一开始就追求「最优」,很多时候「够用」就是最好的。