mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1236 字
3 分钟
空闲链表(Free List)
2026-06-13

一、为什么需要空闲链表#

你写了一个游戏引擎,每帧要创建和销毁数百个实体。每个实体占 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
属性说明
allocO(1)从头部弹出
freeO(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 runtimemfixalloc.go#L31-L109fixalloc 固定大小空闲链表分配器,mlink 结构体是覆盖在已释放块上的侵入式链表节点。驱动 Go 的内存子系统
Linux 内核 SLUBslub.c#L530-L551get_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 - 微软的分片设计分配器,段级空闲链表

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

空闲链表(Free List)
https://blog.souloss.com/posts/programming/memory/memory-free-list/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时