138 lines
4.8 KiB
Markdown
138 lines
4.8 KiB
Markdown
---
|
||
tags: [go, golang, runtime, 内存分配, mcache, mcentral, mheap]
|
||
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 分配:
|
||
|
||
```go
|
||
// 超过 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 内)完全避免锁竞争。
|
||
|
||
## 关联笔记
|
||
|
||
- [[DEV/GO/内存分配与逃逸分析]]
|
||
- [[DEV/GO/GMP调度模型]]
|
||
- [[DEV/GO/GC垃圾回收]]
|