内存分配源码 1:malloc.go 头注释与数据结构
系列阅读:
内存分配->内存分配源码1->内存分配源码2
术语口径:沿用《内存分配》的仓库比喻;本篇出现的mcache、mspan、mcentral、mheap、pageAlloc、heapArena、fixalloc等与之一一对应。
这篇写给谁
- 已经读完通俗篇,知道为什么要分层、小/大/tiny 大致怎么走。
- 想精读
malloc.go文件头注释,并对照各结构体定义,把整份 allocator 的「地图」钉牢。
一句话总览
文件头那段英文注释就是官方地图:小对象沿 mcache → mcentral → mheap → 向 OS 要页;大对象绕过 mcache/mcentral 直走 mheap;再往下是 arena、heapArena 与虚拟地址布局。
导读
本文是 malloc.go 的文件头注释。
这是整份 allocator 的地图,建议先完整读一遍:
小对象:按 size class(约 70 档)→ mcache → mcentral → mheap → 向 OS 要页的层级。
大对象:绕过 mcache/mcentral,直接走 mheap。
数据结构:fixalloc、mheap、mspan、mcentral、mcache、mstats。
虚拟地址:arena、heapArena、arena map(后面还有一段「Virtual memory layout」)
Memory allocator.
This was originally based on tcmalloc, but has diverged quite a bit. goog-perftools.sourceforge.net/doc/tcmallo…
内存分配器。最初基于 tcmalloc,但已大幅偏离。文档见上述链接。
The main allocator works in runs of pages. Small allocation sizes (up to and including 32 kB) are rounded to one of about 70 size classes, each of which has its own free set of objects of exactly that size. Any free page of memory can be split into a set of objects of one size class, which are then managed using a free bitmap.
主分配器以连续页(page run)为单位工作。小对象分配尺寸(含 32 kB 及以下)会向上取整到约 70 个 size class 之一,每个 class 维护一组恰好该尺寸的空闲对象。任意空闲页可被拆成某一 size class 的一组对象,并用空闲位图(free bitmap)管理。
The allocator's data structures are:
fixalloc: a free-list allocator for fixed-size off-heap objects,
used to manage storage used by the allocator.
mheap: the malloc heap, managed at page (8192-byte) granularity.
mspan: a run of in-use pages managed by the mheap.
mcentral: collects all spans of a given size class.
mcache: a per-P cache of mspans with free space.
mstats: allocation statistics.
分配器的数据结构包括:
- fixalloc:定长、堆外对象的空闲链表分配器,用于管理分配器自身占用的存储。
- mheap:malloc 堆,以页(8192 字节)为粒度管理。
- mspan:由
mheap管理的一段在用页。 - mcentral:汇集某一 size class 的全部 span。
- mcache:每个 P 上的缓存,存放仍有空闲的
mspan。 - mstats:分配统计。
Allocating a small object proceeds up a hierarchy of caches:
1. Round the size up to one of the small size classes
and look in the corresponding mspan in this P's mcache.
Scan the mspan's free bitmap to find a free slot.
If there is a free slot, allocate it.
This can all be done without acquiring a lock.
2. If the mspan has no free slots, obtain a new mspan
from the mcentral's list of mspans of the required size
class that have free space.
Obtaining a whole span amortizes the cost of locking
the mcentral.
3. If the mcentral's mspan list is empty, obtain a run
of pages from the mheap to use for the mspan.
4. If the mheap is empty or has no page runs large enough,
allocate a new group of pages (at least 1MB) from the
operating system. Allocating a large run of pages
amortizes the cost of talking to the operating system.
分配小对象沿缓存层级向上申请:
- 将尺寸向上取整到某一小对象 size class,在该 P 的
mcache中查找对应mspan;扫描其 free bitmap 找空闲槽;若有则分配。全程可无锁完成。 - 若该
mspan无空闲槽,从mcentral中「该 size class 且仍有空间」的mspan链表取一块新mspan。一次取整段 span 可摊薄对mcentral加锁的成本。 - 若
mcentral的mspan链表为空,从mheap取一段页作为该mspan。 - 若
mheap为空或没有足够大的连续页,则从操作系统申请新的一组页(至少 1MB)。一次向 OS 申请较大连续页可摊薄系统调用成本。
Sweeping an mspan and freeing objects on it proceeds up a similar hierarchy:
1. If the mspan is being swept in response to allocation, it
is returned to the mcache to satisfy the allocation.
2. Otherwise, if the mspan still has allocated objects in it,
it is placed on the mcentral free list for the mspan's size
class.
3. Otherwise, if all objects in the mspan are free, the mspan's
pages are returned to the mheap and the mspan is now dead.
清扫 mspan 并释放其上对象时,沿类似层级处理:
- 若是因分配需求而清扫该
mspan,则把它交回mcache以满足本次分配。 - 否则,若
mspan上仍有已分配对象,则挂到该mspan所属 size class 的mcentral空闲链表。 - 否则,若
mspan内对象全部空闲,则将其页归还mheap,该mspan视为死亡。
Allocating and freeing a large object uses the mheap directly, bypassing the mcache and mcentral.
大对象的分配与释放直接使用 mheap,不经 mcache 与 mcentral。
If mspan.needzero is false, then free object slots in the mspan are already zeroed. Otherwise if needzero is true, objects are zeroed as they are allocated. There are various benefits to delaying zeroing this way:
1. Stack frame allocation can avoid zeroing altogether.
2. It exhibits better temporal locality, since the program is
probably about to write to the memory.
3. We don't zero pages that never get reused.
若 mspan.needzero 为 false,则 mspan 中空闲对象槽已是零化的;若为 true,则在分配时清零。推迟清零的好处包括:
- 栈帧分配可完全避免清零。
- 时间局部性更好,程序很可能马上会写入这块内存。
- 不会对永远不会再被复用的页做清零。
Virtual memory layout
The heap consists of a set of arenas, which are 64MB on 64-bit and 4MB on 32-bit (heapArenaBytes). Each arena's start address is also aligned to the arena size.
Each arena has an associated heapArena object that stores the metadata for that arena: the heap bitmap for all words in the arena and the span map for all pages in the arena. heapArena objects are themselves allocated off-heap.
Since arenas are aligned, the address space can be viewed as a series of arena frames. The arena map (mheap_.arenas) maps from arena frame number to *heapArena, or nil for parts of the address space not backed by the Go heap. The arena map is structured as a two-level array consisting of a "L1" arena map and many "L2" arena maps; however, since arenas are large, on many architectures, the arena map consists of a single, large L2 map.
The arena map covers the entire possible address space, allowing the Go heap to use any part of the address space. The allocator attempts to keep arenas contiguous so that large spans (and hence large objects) can cross arenas.
虚拟内存布局
堆由一组 arena 组成:64 位上为 64MB,32 位上为 4MB(heapArenaBytes)。每个 arena 的起始地址按 arena 尺寸对齐。
每个 arena 对应一个 heapArena,保存该 arena 的元数据:arena 内所有字的堆位图(heap bitmap),以及所有页的 span 映射。heapArena 本身在堆外分配。
因 arena 对齐,地址空间可看作一串 arena 帧。arena 映射(mheap_.arenas)从 arena 帧编号映射到 *heapArena;非 Go 堆支撑的地址空间为 nil。该映射为两级数组:一层「L1」与多个「L2」arena 表;但因 arena 很大,在许多架构上实际只有一个大的 L2 表。
arena 映射覆盖整个可寻址空间,使 Go 堆可使用地址空间任意部分。分配器尽量让 arena 在物理上连续,以便大 span(从而大对象)可跨 arena。
Go堆在干什么?
程序不停 new / 字面量 / slice 增长,运行时要在虚拟地址里划出一块块内存,还要配合 GC 知道哪块还能用、哪块该回收。 这些结构就是分工:谁管快路径、谁管全局、谁管页、谁管元数据,避免「全堆一把大锁 + 每次问操作系统」 用「仓库」类比(小对象)
| 名字 | 干嘛用 | 昵称 |
|---|---|---|
| size class | 小对象不按精确字节分配,而是向上取整到约 70 档固定尺寸(像 S/M/L 码),这样同一档里每块一样大,好管理、好找空位。 | 尺码档 |
| mspan | 一段连续内存页,要么切成很多同尺寸小槽,要么一整段给一个大对象 | 托盘 / 货架 |
| mcache | 每个 P(跑 goroutine 的调度上下文)一份本地小仓库。小对象优先从这里拿,尽量不抢全局锁。 | 口袋 |
| mcentral | 某一种 size class 的全局中转站:本地 mcache 没货了,来这里领一整块 mspan,少锁几次、一次多拿点。 | 中心补给站 |
| mheap | 所有 span 从哪来、页怎么向 OS 要/还、每个 size class 的 mcentral 数组也挂在这里。 | 总仓 |
| pageAlloc(在 mheap.pages 里) | 按「页」记账:哪段地址连续空着、从哪开始找连续页——给拆 span、大对象、向系统 grow 用,和「小对象槽位里的 bitmap」是不同粒度。 | 页账本 |
| arena + heapArena | 堆地址按大块(如 64MB)切成 arena;heapArena 是这一块里的目录:这一页属于哪个 span、GC/分配用的位图等。通过地址快速查到元数据。 | 街区 + 户口簿 |
| fixalloc | mspan、mcache 这些控制块不能和普通 malloc 抢同一套逻辑,否则鸡生蛋蛋生鸡,所以用定长块 + sysAlloc 单独管。 | 元数据专仓 |
| mstats | 分配了多少之类,偏观测,不参与主路径逻辑。 | 统计数字 |
小对象路径可以记成:先翻自己口袋(mcache)→ 没有就去班组仓库领一箱(mcentral)→ 还没有就让总仓划新托盘(mheap / pageAlloc)→ 再不行向厂里订货(OS,一次多要点)。
大对象:跳过 mcache/mcentral,直接走 mheap(页级),因为太大不值得塞进「按档小槽」那套。
数据结构
mcache
mcache = 每个 P 一份的小对象分配快取(里面主要是按 size class 缓存的 mspan、以及 tiny 等状态)。 在堆外(NotInHeap),专门给 malloc 小对象 走快路径用的。 每个 P 有一个 mcache 指针(P.mcache)
// 无需加锁:每个线程(每个 P)一份。
type mcache struct {
_ sys.NotInHeap
...
// Tiny 分配器缓存:无指针的极小对象。见 malloc.go「Tiny allocator」。
// tiny:当前 tiny 块起始;无当前块则为 nil。堆指针;mcache 不在 GC 扫描的内存里,
// 在 mark termination 的 releaseAll 中会清空。
// tinyoffset:当前块内下一个可分配位置(已用末尾偏移)。
// tinyAllocs:拥有本 mcache 的 P 已执行的 tiny 分配次数。
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
// 其余字段不是每次 malloc 都访问。
alloc [numSpanClasses]*mspan // 按 spanClass 索引,从哪些 mspan 上分配
stackcache [_NumStackOrders]stackfreelist // 栈空闲链表缓存(按栈阶)
// flushGen:上次 flush 时对应的 sweep 代(sweepgen)。
// 若 flushGen != mheap_.sweepgen,则本 mcache 中的 span 已过期,须先被 flush 才能正确清扫。
// 在 acquirep 等路径处理。
flushGen atomic.Uint32
}
mspan
mspan 是一段连续页(page run),装小对象槽或大对象,挂在 mheap / mcentral / mcache 的链表上。 哪个槽空:靠 allocBits 分配位图和 freeindex 起扫,allocCache 用来加速找空位。 GC:同一 span 还有 gcmarkBits 等,和 sweepgen 一起参与清扫。
// mspan:连续页的一段;小对象按槽切分,大对象可整段是一个大对象。
type mspan struct {
_ sys.NotInHeap
next *mspan // 链表后继
prev *mspan // 链表前驱
list *mSpanList // 调试等
startAddr uintptr // 起始地址,即 base()
npages uintptr // 连续页数
// freeindex:从第几个槽开始扫 allocBits 找下一个空闲槽;== nelems 表示已满。
freeindex uint16
nelems uint16 // 槽个数
...
}
mcentral
mcentral 是全局的、按 spanClass 分的中心,每个 size class(含 noscan 组合)一份。 mcache 缺 span 时来这里批量拿,摊薄锁;内部有 partial、full 两套 spanSet,已清扫和未清扫随 GC 换角色。
// 某一 size class 的中心空闲链表。
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
// A spanSet is a set of *mspans
partial [2]spanSet
full [2]spanSet
}
mheap
mheap 是全局唯一的堆对象(变量 mheap_),管所有 span、arena 映射、按页向 OS 要还内存(经 pageAlloc)、以及每个 spanClass 的 mcentral 数组。 锁是 mheap.lock,注释要求只在系统栈上获取,避免 G 栈增长时持锁自死锁。
type mheap struct {
_ sys.NotInHeap
// lock:须在系统栈上获取,否则 G 可能因栈增长在持锁时自死锁。
lock mutex
pages pageAlloc // 按页分配(空闲页摘要 + 位图)
sweepgen uint32 // 清扫世代,见 mspan
// 曾创建过的全部 mspan,各出现一次; backing 可随堆增长重分配。
allspans []*mspan
// 比例清扫、reclaimIndex、heapArenas、curArena、userArena 等:见 mheap.go
// arena 映射:虚拟地址按 arena 帧 → heapArena 元数据;未映射区域为 nil。
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
heapArenas []arenaIdx // 已映射的 heap arena 索引列表
// 每个 spanClass 一个 mcentral;中间有 padding,避免各 mcentral.lock 同缓存行抢行。
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
spanalloc fixalloc // 分配 *mspan 等元对象;其它 cachealloc、special*alloc 等同理
}
pageAlloc
pageAlloc 是 mheap.pages 的类型,在已保留地址范围内用 radix 摘要 summary 和两级稀疏位图 chunks 维护页级空闲,用 searchAddr 做地址有序查找。 和小对象 free bitmap 不同:这里是操作系统页粒度,给 mheap 拆 span、大对象、向 OS grow 用。
type pageAlloc struct {
// summary:各级摘要,快速判断一大段是否全空、全满或混合。
summary [summaryLevels][]pallocSum
// chunks:两级稀疏数组,按 chunk 存页位图;64 位上常见 1MiB 一块等,见注释表格。
chunks [1 << pallocChunksL1Bits]*[1 << pallocChunksL2Bits]pallocData
// searchAddr:下次从哪一带开始找连续空闲页;耗尽时可取 maxOffAddr。
searchAddr offAddr
// start/end、scavenge 等:见 mpagealloc.go
}
heapArena
heapArena 是每个 arena(如 64MB)一块的元数据,在堆外,经 mheap_.arenas 索引。 快查:spans 数组按本 arena 内页号直接得到 mspan;还有 pageInUse、pageMarks 等位图服务分配与 GC。
// 每个 heap arena 的元数据;通过 mheap_.arenas 索引访问。
type heapArena struct {
_ sys.NotInHeap
// spans:本 arena 内每个虚拟页对应 *mspan;从未分配的页为 nil。
spans [pagesPerArena]*mspan
// pageInUse:哪些 span 处于 mSpanInUse(每 span 只标首页)。
pageInUse [pagesPerArena / 8]uint8
// pageMarks:页上是否有标记过的对象(清扫时快速判断整 span 是否可释放)。
pageMarks [pagesPerArena / 8]uint8
// pageSpecials、pageUseSpanInlineMarkBits、checkmarks、zeroedBase 等见源码
zeroedBase uintptr // 从未用过、已为零的区域起点,用于推迟清零判断
}
fixalloc
fixalloc 是定长空闲链表分配器,从 sysAlloc 拉大块再切开;不经过 mallocgc,避免分配器自举时 mspan、mcache 再走普通堆形成循环依赖。 返回是否清零由 zero 控制;首字在释放时会被破坏作链表,调用方自己加锁。
// 定长对象空闲链表;malloc 用 fixalloc + sysAlloc 管理 mcache、mspan 等元数据。
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // 某块第一次分配给用户时回调
arg unsafe.Pointer
list *mlink // 空闲链表
chunk uintptr // 当前 chunk(刻意 uintptr 减少写屏障)
nchunk uint32 // 当前 chunk 剩余字节
nalloc uint32 // 新 chunk 大小
inuse uintptr // 当前在用字节
stat *sysMemStat
zero bool // 是否清零分配
}