160 lines
5.9 KiB
Markdown
160 lines
5.9 KiB
Markdown
|
|
---
|
|||
|
|
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 如何配合内存管理
|