高效堆分配器是现代系统软件性能瓶颈的关键优化点,尤其在低延迟高吞吐场景如服务器、游戏引擎或 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,提升局部性。
  • 落地清单
    1. Size table 预计算:uint8_t size_to_class [1<<16];class=table [request_size]。
    2. Span header(64B/cacheline):npages, class, freelist_head, alloc_count, state。
    3. Partial spans 分类:central_free [67][3](empty/partial/full stacks)。
    4. 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。
  • 落地清单
    1. Scavenger goroutine:每 1s scan 1% spans,release 空 hugepage。
    2. Page age:timestamp last_touch,>5min candidate。
    3. Throttle:release_rate < 1% total_heap/sec,避免 thrashing。
    4. 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 伪码,详见参考。

资料来源