This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/DEV/GO/内存分配与逃逸分析/三级缓存架构.md
T

138 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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垃圾回收]]