mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1088 字
3 分钟
Arena 分配器(Arena Allocator)
2026-06-13

一、为什么需要 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 创建指定容量的 Arena
func 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 bumpalolib.rs#L378-L383Bump 结构体持有 bump 指针,try_alloc_layout_fast 是热路径:读指针、对齐、减去大小、检查容量。被 wasm-bindgen、Rust 编译器和 Deno 使用
Go 实验性 Arenaarena.go#L44-L67实验性 Arena 类型,New[T]() 从 Arena 分配,Free() 一次性释放绕过 GC
Zig 标准库ArenaAllocator.zigZig 的 std.mem.ArenaAllocator 作为核心分配器模式,编译期即可确定使用方式

七、小结#

何时使用:

  • 编译器/解析器——解析期间分配 AST 节点,编译后一次性释放
  • 游戏帧数据——每帧分配,帧边界重置
  • 请求级数据——Web 服务器为单个请求分配的临时数据
  • 序列化——编码/解码过程中的临时缓冲区

何时不用:

  • 长生命周期对象——Arena 一次性释放所有内容,不能单独保留
  • 生命周期差异大——不同对象需要在不同时间释放,Arena 无法满足
  • 分配大小不可预测——Arena 可能浪费大量空间
  • 需要线程共享——无同步机制时 Arena 不是线程安全的

八、参考资料#

支持与分享

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

Arena 分配器(Arena Allocator)
https://blog.souloss.com/posts/programming/memory/memory-arena-allocator/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时