5.9 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-06-07 15:55 |
内存管理机制
概述
本文从 TCMalloc 的设计思想出发,解析 Go 堆内存管理的三层架构(mheap → mcentral → mcache),以及小对象和大对象的分配策略。理解内存管理能帮你优化程序的内存使用、减少 GC 压力。
[!question] ❓ 思考 为什么 Go 的内存管理器要分成三层?每个 P 都配一个 mcache 真的是为了无锁访问吗?超过 32KB 的大对象为什么不走 mcache 而直接去 mheap 拿?
正文
一、设计渊源:TCMalloc
Go 的内存管理借鉴了 Google 的 TCMalloc(Thread-Caching Malloc)思想。TCMalloc 的核心设计是"以空间换时间"——用多层缓存避免锁竞争。
flowchart LR
ThreadCache["ThreadCache<br/>每个线程独立,无锁"] --> CentralCache["CentralCache<br/>所有线程共享,加锁"]
CentralCache --> PageHeap["PageHeap<br/>OS 内存页管理"]
style ThreadCache fill:#e8f5e9
style CentralCache fill:#fff9c4
style PageHeap fill:#ffebee
TCMalloc 的基本单位:
| 层级 | 名称 | 说明 |
|---|---|---|
| 最底层 | Page | 与 OS 内存页对应,通常是 4KB |
| 中层 | Span | 一组连续的 Page,是内存管理的基本单位 |
| 上层 | ThreadCache | 每个线程私有的空闲块链表,按大小分类 |
| 顶层 | CentralCache | 所有线程共享,加锁访问 |
二、Go 的内存管理架构
Go 将 TCMalloc 改造为三层层级结构,适配 GMP 模型:
graph TB
subgraph M["用户请求分配"]
Small["小对象 (≤32KB)"]
Large["大对象 (>32KB)"]
end
subgraph SmallPath["小对象路径"]
MC["mcache<br/>每P一个,无锁"] --> MSC["mcentral<br/>全局共享,加锁"]
MSC --> MH["mheap<br/>管理OS页面"]
end
subgraph LargePath["大对象路径"]
MH2["mheap"] --> OS["直接向OS申请"]
end
Small --> SmallPath
Large --> LargePath
style MC fill:#e8f5e9
style MSC fill:#fff9c4
style MH fill:#ffebee
核心组件对照表
| Go 组件 | TCMalloc 对应 | 说明 |
|---|---|---|
mcache |
ThreadCache | 每个 P 私有,无锁访问 |
mcentral |
CentralCache | 全局共享,需加锁 |
mheap |
PageHeap | 管理 OS 页面的树结构 |
[!tip] 💡 为什么是每个 P 而不是每个 M? Go 最多有 GOMAXPROCS 个 M 同时运行,每个 P 配一个 mcache 就能保证所有 M 在各自的 P 上执行时无锁分配。这比 TCMalloc 的"每个线程一个 cache"更精确。
三、Span Class:67 种规格
Go 对小对象按大小分成了 67 个 class,每个 class 对应固定的对象大小和 span 包含的对象数:
class bytes/obj bytes/span objects tail waste
1 8 8192 1024 0 87.50%
2 16 8192 512 0 43.75%
...
17 256 8192 32 0 5.86%
...
58 16384 16384 1 0 12.49%
66 32768 32768 1 0 12.50%
0 >32KB N/A 1 N/A N/A
[!note] 📝 源码要点 Span class 由两个因素决定:(1) 对象大小所在的 size class;(2) 是否包含指针(noscan)。公式:
spanClass = sizeClass<<1 | noscan。这意味着同一个 size class 会有 scan/noscan 两种变体。
分配一个小对象的步骤
// 以分配一个含指针的 20 字节对象为例
size := 20
// Step 1: 根据大小找 size class
sizeclass = size_to_class8[divRoundUp(size, 8)] // = 3 (范围 (16, 32])
// Step 2: 计算 span class (含指针 => noscan=false)
spc = makeSpanClass(sizeclass, false) // = 3<<1|0 = 6
// Step 3: 从当前 P 的 mcache 中获取对应 span
span = c.alloc[spc]
// Step 4: 从 span 中取一个空闲对象
v = nextFreeFast(span)
if v == 0 {
v, span, _ = c.nextFree(spc) // span 不够时向 mcentral 申请
}
四、大对象分配 (>32KB)
大对象不走 mcache/mcentral,直接在 mheap 上分配:
flowchart TD
Req["分配 >32KB 对象"] --> Calc["计算需要的页数和 span class=0"]
Calc --> SearchFree{"搜索 free 树"}
SearchFree -->|找到| Split{span > 需求?}
SearchFree -->|未找到| SearchScav{"搜索 scav 树<br/>已扫描的空闲 span"}
SearchScav -->|找到| Split
SearchScav -->|未找到| OSAlloc["向 OS 申请 mmap"]
OSAlloc --> ReSearch["重新搜索 free + scav"]
ReSearch --> Split
Split -->|是| Divide["分割 span<br/>一部分给请求,余下放回 free"]
Split -->|否| Done["直接使用"]
Divide --> Done
style Req fill:#e3f2fd
style Done fill:#e8f5e9
[!warning] ⚠️ 注意 频繁分配超大对象会显著增加内存消耗和 GC 负担。建议对大数据使用
[]byte复用或对象池(sync.Pool)。
五、Tiny Object 优化
Go 还有一个特殊类别:Tiny Object(1~16 字节且不含指针)。多个 tiny object 可以共用同一个 span 中的不同 slot,大幅减少小对象的数量浪费。
[!info] ℹ️ 补充 Tiny object 从 Go 1.9 引入。对于常见的短字符串、小整数包装等场景,tiny object 能将内存利用率提升 50% 以上。
小结
- Go 内存管理基于 TCMalloc,分为 mcache(无锁)→ mcentral(加锁)→ mheap(OS 页面)三层
- 小对象(≤32KB)按 67 种 span class 分类分配,每个 P 的 mcache 实现无锁
- 大对象(>32KB)直接在 mheap 上分配,可能触发 OS mmap
- Tiny object(1~16 字节无指针)可合并存放,节省内存
关联笔记
- hzh/GolangStar/Go语言原理/gmp调度原理 — GMP 模型中 P 与 mcache 的关系
- hzh/GolangStar/Go语言原理/逃逸分析 — 变量分配到栈还是堆
- hzh/GolangStar/Go语言原理/垃圾回收 — GC 如何配合内存管理