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

5.9 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go-principle
memory-management
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 字节无指针)可合并存放,节省内存

关联笔记