Go 内存分配
系列阅读:
内存分配->内存分配源码1->内存分配源码2
术语口径:口袋≈mcache、补给站≈mcentral、总仓≈mheap、货架≈mspan;尺码档即 size class;spanClass 是记在每架货架(mspan)上的档位/类型标签。大件与 tiny 不照搬同一条流水线,见下文「内存分配流程」。
这篇写给谁
- 想先建立「堆上怎么要一块内存」的整体图景,分清小对象 / 大对象 / tiny 的边界。
- 更偏直觉与分工,暂不深抠
runtime源码。
Go 进程的内存区域
| 区域 | 特点 | 存放内容 | 管理方式 |
|---|---|---|---|
| 栈区 (Stack) | 每个 Goroutine (G) 都有自己专属的栈(初始 2KB);栈是动态的,太挤了会换更大块「搬家」。 | 函数里的局部变量、参数、返回地址。 | 编译器自动分配与释放。 |
| 堆区 (Heap) | 即mheap、mspan 那一套。 | 逃逸的对象、new / make 的大数据、全局共享对象。 | GC(垃圾回收器)定期清扫。 |
| 数据段 (Data Segment) | 分两块:.data(已赋初值)、.bss(未初始化,Go 默认刷成 0)。 | 全局变量、静态变量。 | 随进程加载布局建立。 |
| 只读区 (Text / ROData) | 整段只读,运行时改写会触发 Segment Fault。 | Text:编译后的机器指令;ROData:字符串常量(如 "Hello World")、const 等。 | — |
| 运行时元数据 (Runtime Metadata) | 通常不在用户堆里,而在 Off-heap(堆外内存)。 | mcache、mcentral、mheap、GC 位图 (Bitmap)、P 等。 | Runtime 手动申请与管理;GC 不扫描它们。 |
本文主要讲述堆相关的内存分配
概述
程序跑起来会不停地「要新地方」:new、字面量、slice 变长、逃逸到堆上的局部变量,最后都落到运行时的堆分配上。如果每次问内存都全局加锁、每次都找操作系统要一页,性能和延迟都会很难看。
所以 Go 的分配器做成了分层:能就近解决的就近解决(每个调度上下文自带一小份缓存),不够再去「中心」批量拿,再不够才从总堆划页、向系统要空间。这套思路早年借鉴过 tcmalloc,后来已经演化成自己的实现。
堆相关结构
可以把堆想象成一座高效仓库:
货架长什么样?
- 货架 (mspan):内存管理的基本单位。一段连续多页,要么切成同一档的小格子装小对象,要么整段给一件大对象。挂在口袋、补给站、总仓之间流转的
货架内部如何切分?
- 尺码表 (size class):仓库不按精确字节划分,而是统一划分为 67 种固定档位(如 S/M/L);每种补给线只发同一档位的货
- 一整架货被切成等大的格子;例如 16B 档,每一格都是 16B,拿取快、碎片可控。
货物怎么放?
-
口袋 (mcache):每个工人口袋里都有最常用的货,拿取无需排队。
- 每个调度用的 P 自带一份。
- Tiny 对象:< 16B 且无指针。
- Small 对象:16B ~ 32KB。
-
补给站 (mcentral):口袋空了才去对应尺码的补给站补货;同码补给站有很多,减少全厂抢一把锁。
-
大宗区域 (mheap):超大件不占小件那条流水线,直接在总仓空地上按连续页划;也统管从操作系统要来的页。
- Large 对象:> 32KB。
仓库怎么维持运转?
-
页账本 (pageAlloc):总仓里按页记「哪段地址空着、能连续划给新货架」。
-
街区 / 户口簿 (heapArena):堆地址按大块切成很多「街区」,每个街区一本户口簿:这一页归哪个 span、配合 GC 标记等,方便分配器和回收器查。
-
行政库房 (fixalloc):给 mspan、mcache 等的简单分配器,不放你的业务对象。
内存分配流程
想象你是一个 Go 程序,现在执行了 obj := new(Object)(且它走的是典型小对象那条线):
- 「口袋」是否有货?(mcache) 你要领一个 16 字节的对象。你先翻自己的口袋(mcache)。 动作: 看看口袋里有没有对应「16 字节档位」的货架(mspan)。 为什么快: 因为口袋是绑在「当前干活的 P」上的,你拿东西不用跟全厂排队(常见路径无锁)。
- 「口袋」空了,去「补给站」批发(mcentral) 口袋里对应的 16 字节货架用完了。你得跑去中心补给站(mcentral)。 动作: 你说:「给我来一整个满载 16 字节槽位的货架(mspan)!」 代价: 补给站是公用的,这里要排队(加锁)。但你一次批发一整块货架回去,够你用好久,减少了下次排队的机会。
- 「补给站」也没货了,去「总仓」拆页(mheap) 补给站发现 16 字节的货架全卖光了。它转身找总仓(mheap)。 动作: 总仓翻开页账本(pageAlloc),找一段连续的空闲内存(比如 8KB 的一页),把它切成几十个 16 字节的小格子,拼成一个新的货架(mspan)给补给站。
- 「总仓」没地了,找「地主」圈地(OS) 如果总仓连 8KB 的空闲页都找不到了。 动作: 总仓向**操作系统(OS)申请新的连续地址空间(运行时再按规则切成街区、登记到页账本;有时一次会多要一些,摊薄跟系统打交道的次数)。 登记: 拿地之后,要在户口簿(heapArena)**上记下来:这一块地怎么切页、每个页对应谁管,方便分配和 GC 查。
还有两类常见例外:
1. 极小、且不含指针的分配(tiny)
特别小的、回收时不需要顺着里面去扫指针的那类,有时会先塞进一个 16 字节的微盒里,好几个极小对象拼成一盒,再占「一整格货架」。所以你看到源码里 tiny 相关逻辑时,要知道:它是在「拼单」,不一定一上来就对应「某一档尺码的一整格」那条典型故事。
2. 大对象
尺寸超过「小对象」上限的,不再进每个 P 的口袋、也不进按尺码分的补给站,而是直接在总仓按连续页划一整段,这一整段通常只装这一件大货。可以理解为:大件不走小件流水线,避免占满一整个货架却只放一样东西还要反复协调。