Files
cs-note/hzh/GolangStar/Go语言原理/memory management原理.md
T

160 lines
5.9 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, 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<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 模型:
```mermaid
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 两种变体。
#### 分配一个小对象的步骤
```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 树<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 如何配合内存管理