1236 字
3 分钟
空闲链表(Free List)
一、为什么需要空闲链表
你写了一个游戏引擎,每帧要创建和销毁数百个实体。每个实体占 256 字节。通用分配器处理这种场景有两个问题:一是每次 malloc 都要遍历空闲块列表找到合适大小的块,最坏情况下是 O(n);二是频繁分配释放导致碎片——128 字节的空闲块插在两个已用块之间,你的 256 字节实体却用不上。
再比如操作系统内核管理进程控制块(task struct),大小固定,创建和销毁极其频繁。内核不能容忍分配时间不可预测,更不能容忍碎片浪费宝贵的内核内存。
空闲链表直接解决这两个问题:维护一个已释放槽位的链表,分配从头部弹出,释放推入头部,都是 O(1)。因为所有槽位大小相同,不存在碎片问题。
二、现实类比
停车场用一块记事板记录空位。有车来,直接分配清单顶部的空位。有车走,空出的位置记回清单顶部。不需要扫描整个停车场找空位,也不需要担心空位被”切碎”。
三、核心思想
空闲链表通过在空闲槽位内部嵌入链表指针(侵入式),或使用独立索引数组(非侵入式)来跟踪可用内存槽位。alloc() 从头部弹出;free() 推入头部。池内无碎片,无系统调用,可预测的 O(1) 性能。
flowchart LR
HEAD[空闲链表头] --> S3[slot 3]
S3 --> S0[slot 0]
S0 --> S7[slot 7]
S7 --> NULL[null]
A["alloc() → 返回 slot 3"] --> HEAD2[新头 → slot 0]
F["free(slot 5) → slot 5 成新头"] --> HEAD3[新头 → slot 5]
text
空闲链表头
│
▼
┌────────┐ ┌────────┐ ┌────────┐
│ slot 3 │───►│ slot 0 │───►│ slot 7 │───► null
└────────┘ └────────┘ └────────┘
alloc() → 返回 slot 3,头指针移到 slot 0
free(5) → slot 5 成为新头,指向 slot 0
| 属性 | 值 | 说明 |
|---|---|---|
| alloc | O(1) | 从头部弹出 |
| free | O(1) | 推入头部 |
| 碎片 | 无 | 固定大小槽位 |
| 开销 | 每个空闲槽一个指针 | 侵入式或索引数组 |
四、变体与对比
| 模式 | 与空闲链表的关系 | 适用场景 |
|---|---|---|
| Arena 分配器 | Arena 批量释放;空闲链表回收单个槽位 | 所有对象同时失效时用 Arena |
| 对象池 | 对象池内部用空闲链表追踪可用对象 | 同类型对象的高频复用 |
| 环形缓冲区 | 都提供 O(1) 槽位管理,方式不同 | 固定大小队列场景 |
| LRU 缓存 | LRU 用空闲链表回收被淘汰的节点槽位 | 缓存淘汰策略 |
| 墓碑 | 墓碑标记删除,空闲链表负责回收槽位 | 延迟删除后的空间回收 |
侵入式 vs 非侵入式:侵入式将 next 指针存在空闲槽位内部,不占额外内存,但有 use-after-free 风险——如果意外写入已释放的槽位,会破坏链表结构。非侵入式使用独立的索引数组,安全性更好但需要额外空间。Linux 内核的 SLUB 分配器采用侵入式,并通过 XOR 编码指针防御堆损坏攻击。
LIFO vs FIFO:空闲链表通常采用 LIFO(后进先出),因为最近释放的槽位更可能仍在 CPU 缓存中,立即复用能获得更好的缓存命中率。
五、多语言实现
5.1 Go 实现
package freelist
// FreeList 固定大小槽位的空闲链表分配器type FreeList struct { capacity int nextSlot int // 下一个从未使用过的槽位 free []int // 已释放的槽位栈}
func NewFreeList(capacity int) *FreeList { return &FreeList{ capacity: capacity, free: nil, }}
// Alloc 分配一个槽位,返回槽位索引// 优先复用已释放的槽位(LIFO,缓存友好)func (fl *FreeList) Alloc() (int, bool) { if len(fl.free) > 0 { // 从栈顶弹出最近释放的槽位 slot := fl.free[len(fl.free)-1] fl.free = fl.free[:len(fl.free)-1] return slot, true } if fl.nextSlot >= fl.capacity { return 0, false // 容量耗尽 } slot := fl.nextSlot fl.nextSlot++ return slot, true}
// Free 释放槽位,推入空闲栈func (fl *FreeList) Free(slot int) { fl.free = append(fl.free, slot)}
// Available 返回可用槽位数量func (fl *FreeList) Available() int { return len(fl.free) + (fl.capacity - fl.nextSlot)}
// Allocated 返回已分配的槽位数量func (fl *FreeList) Allocated() int { return fl.nextSlot - len(fl.free)}5.2 TypeScript 实现
class FreeList { private freeSlots: number[] = []; private nextSlot: number;
constructor(private capacity: number) { this.nextSlot = 0; }
// 分配槽位:优先复用已释放的 alloc(): number | null { if (this.freeSlots.length > 0) { return this.freeSlots.pop()!; } if (this.nextSlot >= this.capacity) { return null; // 容量耗尽 } return this.nextSlot++; }
// 释放槽位:推入空闲栈顶 free(slot: number): void { this.freeSlots.push(slot); }
get available(): number { return this.freeSlots.length + (this.capacity - this.nextSlot); }
get allocated(): number { return this.nextSlot - this.freeSlots.length; }}
// 使用示例:实体管理器const entitySlots = new FreeList(1000);const entities: (object | null)[] = new Array(1000).fill(null);
function spawnEntity(data: object): number | null { const slot = entitySlots.alloc(); if (slot === null) return null; entities[slot] = data; return slot;}
function destroyEntity(slot: number): void { entities[slot] = null; entitySlots.free(slot);}六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Go runtime | mfixalloc.go#L31-L109 | fixalloc 固定大小空闲链表分配器,mlink 结构体是覆盖在已释放块上的侵入式链表节点。驱动 Go 的内存子系统 |
| Linux 内核 SLUB | slub.c#L530-L551 | get_freepointer / set_freepointer 读写嵌入在每个空闲 slab 对象内部的 next-free 指针,使用 XOR 编码防御堆损坏攻击 |
| jemalloc | 源码 | 小分配的线程缓存空闲链表,减少锁竞争 |
七、小结
何时使用:
- 游戏引擎——实体/组件池的快速分配和释放循环
- 操作系统内核——固定大小内核对象的 slab 分配器(inode、task 结构体)
- 嵌入式系统——无堆、确定性分配时序
- 数据库引擎——B 树节点的页面分配器
何时不用:
- 可变大小对象——空闲链表需要固定大小槽位,用通用分配器替代
- 很少释放——对象永生时空闲链表始终为空,用 bump/arena 分配器更好
- 小型池——低于约 16 个槽位时管理链表的开销超过收益
- 需要线程安全——基本空闲链表需要外部同步(或使用无锁变体)
八、参考资料
- Go runtime fixalloc - Go 运行时固定大小分配器,侵入式空闲链表实现
- Linux SLUB 分配器 - 内核 slab 分配器,XOR 编码空闲链表指针防御攻击
- jemalloc - Facebook 的高性能分配器,线程缓存空闲链表
- mimalloc - 微软的分片设计分配器,段级空闲链表
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
Arena 分配器(Arena Allocator)
程序设计 整块申请内存,用完一次性释放——bump pointer 分配的极致性能,编译器和游戏引擎的核心内存策略。
2
Slab 分配器(Slab Allocator)
程序设计 固定大小对象池,消除碎片——Linux 内核和 Memcached 的高频对象分配策略
3
享元(Flyweight)
程序设计 共享不可变的部分,只存储变化的部分——用更少的对象表示更多的数据,游戏引擎和编译器中的经典内存优化。
4
伙伴系统(Buddy System)
程序设计 2 的幂次方分裂合并内存块——Linux 页分配和 DMA 缓冲区的核心算法
5
驻留(Interning)
程序设计 值相同的对象只保留一份——字符串和符号去重的经典技术,用 O(1) 的指针比较替代 O(n) 的内容比较。






