1088 字
3 分钟
Arena 分配器(Arena Allocator)
一、为什么需要 Arena 分配器
写一个 JSON 解析器,解析过程中会创建成千上万个 AST 节点——字符串值、数字值、数组节点、对象节点。解析完成后,这些节点被遍历一次,提取出业务数据,然后整个 AST 就没用了。
问题来了:你用通用分配器逐个 malloc 这些节点,就得逐个 free。写一个编译器,解析阶段产生的中间节点、类型信息、符号引用……生命周期都一样——编译完了统统丢弃。逐个释放不仅繁琐,还容易遗漏。
更糟的是碎片化。通用分配器反复分配和释放不同大小的块,内存中留下大量无法复用的碎片。长时间运行的服务,碎片化可能浪费 30% 甚至更多的内存。
Arena 分配器提供了另一种思路:整块申请内存,用指针往前推来分配,用完后一次性释放整块。没有逐对象释放,没有碎片,分配速度接近理论的 O(1) 上限。
二、现实类比
会议室的白板。所有人在有空的地方随意写,笔触不断往前推进。会议结束时,整块白板一次性擦干净——不需要逐条擦掉每条笔记,也不需要担心某条笔记占了别人想用的位置。
三、核心思想
Arena 预分配一块连续内存,通过推进偏移指针来分发内存块。单个分配无法单独释放——整个 Arena 在不用时一次性释放。这消除了逐对象分配开销、碎片化和 GC 压力。
flowchart LR
A[alloc 16B] --> B[alloc 8B] --> C[alloc 32B] --> D[reset]
subgraph Arena 内存布局
direction LR
B1[obj1\n16B] --- B2[obj2\n8B] --- B3[obj3\n32B] --- B4[空闲空间]
end
D --> E[偏移归零\n所有对象瞬间释放]
Arena: [ capacity ]┌──────┬──────┬──────┬────────────────────────┐│ obj1 │ obj2 │ obj3 │ free space │└──────┴──────┴──────┴────────────────────────┘▲└── offset(bump 指针)
alloc(16) → offset: 0→16 (返回区域 0..16)alloc(8) → offset: 16→24 (返回区域 16..24)reset() → offset: 0 (所有对象瞬间释放)| 属性 | 值 | 说明 |
|---|---|---|
| 分配速度 | O(1) | 仅移动指针 |
| 释放 | O(1) | 重置指针,释放全部 |
| 单独释放 | 不支持 | 需要配合 free-list 或 GC |
| 碎片化 | 无 | 连续分配,无空隙 |
四、变体与对比
| 模式 | 与 Arena 的关系 | 适用场景 |
|---|---|---|
| 空闲链表(Free List) | 空闲链表回收单个对象;Arena 一次性批量释放 | 需要单独释放对象的场景 |
| 对象池(Object Pool) | 对象池预分配固定类型;Arena 推进指针分配任意大小 | 同类型对象的高频复用 |
| 引用计数 | Arena 通过作用域结束释放避免逐对象引用计数 | 共享所有权的资源管理 |
| 通用分配器 | 通用分配器逐个分配和释放,产生碎片 | 生命周期各异的通用场景 |
五、多语言实现
5.1 Go 实现
package arena
// Arena 基于 bump pointer 的区域分配器type Arena struct { buf []byte offset int}
// NewArena 创建指定容量的 Arenafunc NewArena(capacity int) *Arena { return &Arena{ buf: make([]byte, capacity), }}
// Alloc 分配 size 字节,返回对应的切片// 分配仅需移动 offset,复杂度 O(1)func (a *Arena) Alloc(size int) []byte { if a.offset+size > len(a.buf) { return nil // 容量不足 } start := a.offset a.offset += size return a.buf[start : start+size]}
// AllocAligned 分配对齐的内存块func (a *Arena) AllocAligned(size, align int) []byte { // 计算对齐后的起始位置 aligned := (a.offset + align - 1) &^ (align - 1) if aligned+size > len(a.buf) { return nil } a.offset = aligned + size return a.buf[aligned:aligned+size]}
// Reset 重置偏移指针,所有已分配的内存瞬间回收func (a *Arena) Reset() { a.offset = 0}
// Used 返回已使用的字节数func (a *Arena) Used() int { return a.offset}使用示例——编译器 AST 解析:
func parseJSON(input []byte) { // 为整个解析过程创建 Arena a := arena.NewArena(len(input) * 2) defer a.Reset() // 解析完成后一次性释放
// 解析过程中所有临时节点都从 Arena 分配 node1 := a.Alloc(64) // AST 节点 node2 := a.Alloc(32) // 字符串值 // ... 解析逻辑 ... // 不需要逐个释放,Reset 统一回收}5.2 TypeScript 实现
class Arena { private buffer: ArrayBuffer; private view: DataView; private offset = 0;
constructor(capacity: number) { this.buffer = new ArrayBuffer(capacity); this.view = new DataView(this.buffer); }
// 分配 size 字节,返回起始偏移 alloc(size: number): { start: number; size: number } | null { if (this.offset + size > this.buffer.byteLength) { return null; // 容量不足 } const start = this.offset; this.offset += size; return { start, size }; }
// 写入和读取 32 位无符号整数 writeU32(offset: number, value: number): void { this.view.setUint32(offset, value); }
readU32(offset: number): number { return this.view.getUint32(offset); }
// 重置:所有分配瞬间失效 reset(): void { this.offset = 0; }
get used(): number { return this.offset; }
get capacity(): number { return this.buffer.byteLength; }}六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Rust bumpalo | lib.rs#L378-L383 | Bump 结构体持有 bump 指针,try_alloc_layout_fast 是热路径:读指针、对齐、减去大小、检查容量。被 wasm-bindgen、Rust 编译器和 Deno 使用 |
| Go 实验性 Arena | arena.go#L44-L67 | 实验性 Arena 类型,New[T]() 从 Arena 分配,Free() 一次性释放绕过 GC |
| Zig 标准库 | ArenaAllocator.zig | Zig 的 std.mem.ArenaAllocator 作为核心分配器模式,编译期即可确定使用方式 |
七、小结
何时使用:
- 编译器/解析器——解析期间分配 AST 节点,编译后一次性释放
- 游戏帧数据——每帧分配,帧边界重置
- 请求级数据——Web 服务器为单个请求分配的临时数据
- 序列化——编码/解码过程中的临时缓冲区
何时不用:
- 长生命周期对象——Arena 一次性释放所有内容,不能单独保留
- 生命周期差异大——不同对象需要在不同时间释放,Arena 无法满足
- 分配大小不可预测——Arena 可能浪费大量空间
- 需要线程共享——无同步机制时 Arena 不是线程安全的
八、参考资料
- Rust bumpalo 源码 - Rust 生态最流行的 Arena 分配器实现
- Go 实验 Arena 提案 - Go 官方 Arena API 设计讨论
- Zig ArenaAllocator - Zig 语言中 Arena 作为核心分配器模式
- V8 Zone 分配器 - V8 引擎为编译器临时对象提供 Arena 风格的 bump 分配
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
Arena 分配器(Arena Allocator)
https://blog.souloss.com/posts/programming/memory/memory-arena-allocator/ 部分信息可能已经过时
相关文章 智能推荐
1
Slab 分配器(Slab Allocator)
程序设计 固定大小对象池,消除碎片——Linux 内核和 Memcached 的高频对象分配策略
2
空闲链表(Free List)
程序设计 用链表管理已释放的内存块,O(1) 分配和释放——操作系统内核和游戏引擎的底层分配利器。
3
享元(Flyweight)
程序设计 共享不可变的部分,只存储变化的部分——用更少的对象表示更多的数据,游戏引擎和编译器中的经典内存优化。
4
伙伴系统(Buddy System)
程序设计 2 的幂次方分裂合并内存块——Linux 页分配和 DMA 缓冲区的核心算法
5
分代垃圾回收(Generational GC)
程序设计 对象分代,年轻代频繁回收——JVM、V8、Go 运行时的核心内存管理策略






