Heap

目录

堆的概念

堆是程序运行时动态申请内存的地方,就像一个大仓库,可以随时借地方存放数据,用完后需要手动归还。

示意图:

┌─────────────────┐
│     堆空间      │
│                 │
│  [空闲区域]     │
│  [已使用块]     │
│  [空闲区域]     │
│  [已使用块]     │
└─────────────────┘

堆与栈的区别

内存布局对比:

┌─────────────────┐
│      栈         │ ← 自动管理,像整齐书架
│  后进先出       │   编译器自动分配释放
├─────────────────┤
│      堆         │ ← 手动管理,像杂货仓库
│  随意存取       │   需要自己申请和释放
└─────────────────┘

栈的特点: - 自动分配和释放 - 大小固定 - 访问速度快

堆的特点: - 手动申请和释放 - 空间灵活 - 访问速度较慢

基本堆管理机制

空闲块链表

系统维护一个空闲内存块的链表,申请内存时从这个链表中查找合适的块。

示意图:

空闲链表:头 → [块A:32字节] → [块B:16字节] → [块C:24字节] → NULL

堆内存:
[已用][块A-空闲][已用][块B-空闲][已用][块C-空闲]

堆内存分配和释放的过程

内存分配过程

当程序申请内存时,分配器在空闲链表中寻找足够大的块。

分配步骤: 1. 在空闲链表中搜索合适大小的块 2. 找到后,从该块中分割出需要的大小 3. 剩余部分形成新的空闲块 4. 更新空闲链表

分配示意图:

初始状态:
空闲链表:[块A:32字节] → [块B:16字节]

申请20字节:
1. 找到块A(32字节)
2. 分割:20字节(已用) + 12字节(新空闲块)
3. 结果:[已用20B] [空闲12B] [空闲16B]

内存释放过程

当程序释放内存时,需要将内存块重新加入空闲链表。

释放步骤: 1. 将释放的块标记为空闲 2. 检查相邻块是否也是空闲 3. 如果相邻空闲,进行合并操作 4. 将合并后的块加入空闲链表

释放示意图:

初始状态:
[已用A][空闲B][已用C][空闲D]

释放块A:
[空闲A][空闲B][已用C][空闲D]
合并A和B:
[大空闲AB][已用C][空闲D]

内存碎片化的过程

碎片化如何产生

随着多次分配和释放,内存逐渐被分割成许多小块。

碎片化过程示意图:

初始: [大空闲块128字节]

分配32B: [已用32][空闲96]
分配24B: [已用32][已用24][空闲72]  
分配16B: [已用32][已用24][已用16][空闲56]

释放24B: [已用32][空闲24][已用16][空闲56]
分配20B: [已用32][已用20][碎片4][已用16][空闲56]

现在有:2个空闲块(4B + 56B),但无法分配60B的请求

内存碎片问题

内部碎片

分配的内存比实际需要的大,多出来的部分被浪费。

示意图:

申请10字节,分配16字节:
[块头|10字节数据|6字节浪费]
         ↑
     内部碎片

外部碎片

空闲内存被分割成很多小块,无法满足大内存申请。

示意图:

堆状态:
[已用][空闲8B][已用][空闲12B][已用][空闲6B]

申请20字节:失败!
总空闲=26字节,但都不连续

常见分配算法

首次适应算法

从链表开头开始查找,找到第一个足够大的块就分配。

工作过程:

空闲链表:[25B] → [18B] → [32B] → [12B]
申请20字节:
检查25B → 足够大,分配
结果:[已用20B|碎片5B] → [18B] → [32B] → [12B]

最佳适应算法

查找与申请大小最接近的空闲块,减少浪费。

工作过程:

空闲块:8B, 25B, 15B, 30B
申请12字节:
找到最匹配的15B块
结果:8B, [已用12B|碎片3B], 30B

RTOS中的堆管理

单一堆管理

整个系统共用一个堆空间,所有任务都从这里申请内存。

示意图:

   任务A   任务B   任务C
     ↓      ↓      ↓
   ┌───────────────┐
   │   系统堆      │
   └───────────────┘

固定大小内存池

预分配很多相同大小的内存块,分配时直接给出一个空闲块。

示意图:

内存池:
[块1][块2][块3][块4][块5][块6]
 │    │    │    │    │    │
32B  32B  32B  32B  32B  32B

优点:分配速度快,无外部碎片
缺点:可能内部碎片

常见错误类型

内存泄漏

申请内存后忘记释放,导致可用内存越来越少。

示意图:

申请 → [内存块] → 忘记释放
申请 → [内存块] → 忘记释放
...
最终:内存耗尽

使用已释放内存

释放内存后继续使用,导致数据损坏或程序崩溃。

示意图:

释放 → [内存块] ✅
使用 → [同一内存块] ❌ 危险!

重复释放

对同一块内存释放两次,通常导致程序崩溃。

示意图:

释放 → [块A] ✅
再次释放 → [块A] ❌ 程序崩溃