---
tags: [go, golang, go-principle, memory-management]
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 的核心设计是"以空间换时间"——用多层缓存避免锁竞争。
```mermaid
flowchart LR
ThreadCache["ThreadCache
每个线程独立,无锁"] --> CentralCache["CentralCache
所有线程共享,加锁"]
CentralCache --> PageHeap["PageHeap
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 模型:
```mermaid
graph TB
subgraph M["用户请求分配"]
Small["小对象 (≤32KB)"]
Large["大对象 (>32KB)"]
end
subgraph SmallPath["小对象路径"]
MC["mcache
每P一个,无锁"] --> MSC["mcentral
全局共享,加锁"]
MSC --> MH["mheap
管理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 两种变体。
#### 分配一个小对象的步骤
```go
// 以分配一个含指针的 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 上分配:
```mermaid
flowchart TD
Req["分配 >32KB 对象"] --> Calc["计算需要的页数和 span class=0"]
Calc --> SearchFree{"搜索 free 树"}
SearchFree -->|找到| Split{span > 需求?}
SearchFree -->|未找到| SearchScav{"搜索 scav 树
已扫描的空闲 span"}
SearchScav -->|找到| Split
SearchScav -->|未找到| OSAlloc["向 OS 申请 mmap"]
OSAlloc --> ReSearch["重新搜索 free + scav"]
ReSearch --> Split
Split -->|是| Divide["分割 span
一部分给请求,余下放回 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 如何配合内存管理