4.8 KiB
4.8 KiB
tags, create time
| tags | create time | |||||||
|---|---|---|---|---|---|---|---|---|
|
2026-04-24 10:30 |
三级缓存架构详解
概述
Go 的内存分配器采用 三级缓存 + 中央池 + 大对象快速通道 的分层架构,核心理念是"让最常见的操作最快"。本文通过生活化类比和底层实现两层视角,帮你彻底理解这个设计。
生活化类比:连锁便利店的仓储体系
🏬 个人抽屉 → mcache
每个店长(P / Goroutine)都有自己的抽屉,里面常备着最常用的商品(固定大小的小物件)。
- 不需要请示任何人,直接伸手拿,零等待
- 抽屉空间有限,装不了太多
- 90% 的日常需求在这里就满足了
📦 门店后仓 → mcentral
店长的抽屉快空了,去后仓拿。
- 后仓按商品类别整齐排列(8 字节的放一起、16 字节的放一起……共 67 个类别)
- 拿的时候可能需要稍等(要拿钥匙 / 找店员,对应细粒度锁)
- 容量比抽屉大得多
🏭 区域总仓 → mheap
门店后仓也空了?打电话给总仓库。
- 总仓库什么都有,容量巨大
- 但手续繁琐(全局锁,要等)
- 只有实在缺货时才走这条路
🚚 超大订单(>32KB)→ 专车直送
如果你要订 1000 箱货(大对象),店长懒得经手中转,直接从总仓库专车送到你店里,跳过抽屉和后仓。
为什么这么设计?
核心思想就是:让最常见的操作最快。
| 场景 | 占比 | 处理方式 | 等待时间 |
|---|---|---|---|
| 小物件、同一个 goroutine | ~90% | 抽屉直接拿 | 0 |
| 偶尔缺货 | ~9% | 去后仓拿 | 轻微 |
| 真正缺大牌货 | ~1% | 找总仓库 | 明显 |
对比一下,如果所有东西都要去总仓库拿(比如某些语言的每次 malloc 都全局加锁),那整个体系就堵死了。
底层实现
第一级:mcache(线程级,无锁)
每个 P 拥有独立的 mcache,是 Goroutine 最近的内存分配点。
- 无锁设计:因为每个 P 独占自己的 mcache,无需竞争
- 缓存固定大小的对象:每种大小类(size class)缓存一个 object 链表
- Go 定义了 67 种对象大小类别,覆盖 0 ~ 32KB 的对象
mcache 内部结构(简化):
mcache.span[class0] → 缓存大小为 8 字节对象的 span
mcache.span[class1] → 缓存大小为 16 字节对象的 span
...
mcache.span[class66] → 缓存大小为 32768 字节对象的 span
第二级:mcentral(池级,细粒度锁)
mcentral 管理多种规格(span class)的内存池,每种规格有独立的 mspan。
- 按大小分组:同样大小的对象放在同一个
mspan中 - 细粒度锁:每种规格的
mspan有独立的锁,而非全局一把大锁 - 当 mcache 的缓存不够时,向 mcentral 请求新的 span
mcentral 内部结构(简化):
mcentral.spans[class0] → mspan (管理大小为 8 字节的对象)
mcentral.spans[class1] → mspan (管理大小为 16 字节的对象)
...
第三级:mheap(堆级,全局锁)
mheap 是整个程序的"大仓库",直接向操作系统申请内存。
- 全局锁保护:申请新内存时需要获取全局锁
- mmap 映射:通过
mmap向 OS 请求内存(初始约 64MB,可增长) - 分配 span 给 mcentral,形成完整的分配链路
mheap 内部结构(简化):
mheap.all[] → 管理所有 span 的数组
mheap.pageAlloc → 位图,标记每个页面是否已分配
大对象分配(>32KB)
超过 32KB 的对象跳过 mcache 和 mcentral,直接从 mheap 分配:
// 超过 32KB 的对象直接走 mheap 路径
func benchmarkLargeAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = make([]byte, 64*1024) // 64KB,直接走 mheap
}
}
// 小于 32KB 的对象走 mcache 路径(更快)
func benchmarkSmallAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = make([]byte, 1024) // 1KB,走 mcache 路径
}
}
思考:为什么 32KB 是一个分界线?这与 CPU 缓存行(cache line,通常 64 字节)和页大小(4KB)有什么关系?
锁粒度对比
| 层级 | 锁粒度 | 竞争程度 | 适用场景 |
|---|---|---|---|
| mcache | 无锁(每个 P 独立) | 零竞争 | 频繁的小对象分配 |
| mcentral | 按大小类细粒度锁 | 低竞争 | 中等频率分配 |
| mheap | 全局锁 | 高竞争 | 大对象分配、扩容 |
设计精妙之处:Go 通过分层 + 无锁设计,让最常见的内存分配(小对象、同一 P 内)完全避免锁竞争。