--- 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 如何配合内存管理