高效堆分配器是现代系统软件性能瓶颈的关键优化点,尤其在低延迟高吞吐场景如服务器、游戏引擎或 AI 推理中。传统 malloc 如 glibc ptmalloc 碎片大、锁竞争重,无法满足需求。本文聚焦单一技术点:arena 布局、span 管理、per-P 缓存与 scavenging 机制的设计与参数,实现无锁 fast path、碎片控制与内存回收。基于 tcmalloc、jemalloc、mimalloc 及 Go allocator 的通用模式,给出可落地参数清单。
Arena 布局:虚拟地址预留与懒分配
Arena 是堆分配器的内存后端,大块连续虚拟地址空间,按页(通常 8KB)细分,支持懒物理页映射,减少 TLB miss 与 OS 开销。
核心观点:预留 hugepage 对齐的 arena(2MB 或 1GB),内部用 spans 数组映射 page→span descriptor,实现 O (1) 指针到元数据的查找。
证据与参数:
- 每个 arena 大小:64MB~1GB,数量动态增长至总虚拟空间上限(如 64 位下数百 GB)。
- 布局:arena_base + page_index * page_size → 对象地址;metadata 区分离(spans [page_index] 存 span ptr,bitmap 存 GC/live 位)。
- 如 Go mheap,每个 heapArena 64MB,包含 65k pages(8KB/page),spans/bitmap 紧邻用户区。
- 落地参数:
参数 值 说明 page_size 8KB 匹配 OS 页,避免 overcommit arena_size 64MB 平衡合并碎片与管理开销 hugepage_align 2MB 启用 THP/HugeTLB,减 TLB 压力 max_arenas 1024 总堆上限~64GB
初始化时 madvise (MADV_HUGEPAGE),运行中后台合并空闲 page 到 hugepage 粒度释放。
Span 管理:size class 与 free list 分片
Span 是连续 pages(1~ 多页),专用于单一 size class,内部 bump 或 free list 切对象。管理空 /partial/full 状态,支持 span 级合并。
核心观点:几何 size class(8B 步进小尺寸,渐粗至 256KB)+ per-span free list,控制 internal frag<20%,external 通过 buddy-like pageheap。
证据与参数:
- Size classes:67 类(Go)或 128 类(jemalloc),tiny (≤16B) 用 bump,small (16B~16KB) span 内链表,large (>16KB) 多页 span。
- Span 生命周期:pageheap alloc npages→init free list(对象数 = npages*page_size /obj_size)→服务 alloc/free→empty 时还 pageheap。
- mimalloc 用 per-page free list sharding,线程倾向同 page alloc,提升局部性。
- 落地清单:
- Size table 预计算:uint8_t size_to_class [1<<16];class=table [request_size]。
- Span header(64B/cacheline):npages, class, freelist_head, alloc_count, state。
- Partial spans 分类:central_free [67][3](empty/partial/full stacks)。
- Frag 监控:span.util = 1 - free_objs/total_objs;>80% 优先服务。
Free 时:ptr→page_index→spans []→span→push freelist(无锁若 local)。
per-P 缓存:无锁 fast path 批量转移
per-P(per-processor/thread)缓存是低延迟核心,每个 P 独享 size class 数组,指向 partial spans,实现 99% alloc/free 本地 O (1)。
核心观点:low/high watermark 控制 refill/drain,batch_size=32~128,中央 mcentral 仅 slow path 锁。
证据与参数:
- Go mcache.alloc [67] 各一 span;空时从 mcentral pop span(锁),否则 local pop obj。
- jemalloc tcache:per-class bin(16~127 objs),full 时 flush 到 arena。
- mimalloc thread heap:per-class active_page ptr + page freelist,无跨线程 atomic fastpath。
- 落地参数:
size_class cache_size batch_transfer tiny/small 64 objs 32 medium 32 objs 16 large 8 spans 4
Alloc 伪码:
class = size_to_class[size];
span = pcache.alloc[class];
if (span.freelist empty) {
span = central[class].pop_partial(); // rare, lock
pcache.alloc[class] = span;
}
return span.freelist.pop();
Free 类似 push,>high_water drain batch 到 central。
NUMA-aware:per-numa central,transfer cache 跨 node batch。
Scavenging 机制:后台异步释放
Scavenging 释放空闲物理页(madvise (DONTNEED)/munmap),防 RSS 膨胀,不挡 alloc path。
核心观点:后台线程 / 定时器扫描 empty spans,优先 hugepage,credit-based 限速。
证据与参数:
- Go scavenger 每 GC 周期或 heap>thresh,scan spans [] 找 idle pages。
- mimalloc abandoned pages:thread exit 时 mark,maintenance slice adopt/scavenge。
- 触发:heap_growth>1.25x steady、idle>10s。
- 落地清单:
- Scavenger goroutine:每 1s scan 1% spans,release 空 hugepage。
- Page age:timestamp last_touch,>5min candidate。
- Throttle:release_rate < 1% total_heap/sec,避免 thrashing。
- Hooks:SIGUSR1 手动 trigger,prom metrics: scavenged_bytes, rss_mb。
监控:alloc_rate, miss_rate<1%, frag<15%, rss/heap<1.5。
风险与回滚
- 风险 1:size class 过细→元数据爆炸;测试调优,fallback glibc。
- 风险 2:scavenge 激进→alloc stall;cap per-cycle 0.1% heap。
参数总结清单:
- Init: arenas=16, classes=67, page=8KB。
- Cache: per-P, batch=32, water=low50%/high90%。
- Scavenge: interval=1s, thresh=heap*1.25, rate=100MB/s。
此设计在生产中可提速 2-5x,RSS 降 30%。如需 C/Rust 伪码,详见参考。
资料来源:
- Mimalloc 技术报告:https://www.microsoft.com/en-us/research/wp-content/uploads/2019/06/mimalloc-tr-v1.pdf (segment≈arena, page sharding)。
- Go 内存分配器剖析:https://golang.design/under-the-hood/zh-cn/part2runtime/ch07alloc/basic/ (mheap/arena/span/mcache)。
- TCMalloc 设计:隐含于多处 benchmark 比较。