Go Runtime(四):内存分配器

本文源码基线为 Go 1.26.2、Linux/amd64、默认 GOEXPERIMENT。对象大小阈值、size class 表和局部快路径可能随版本变化。 本文从两个现象出发:源码里的 new(T) 可能产生零次堆分配,对象失去引用后 RSS 也可能维持原值。第一个现象把我们带入 mallocgc、mcache、mcentral 和 mheap;第二个现象继续追踪 sweep、页分配器与 scavenger。 Go 内存分配器要同时满足三类目标:小对象分配足够快,多 P 并发时减少共享锁竞争,GC 又能由任意堆地址快速定位对象边界和标记元数据。它采用按对象规格分级、按 P 缓存、按页统一管理的结构。 把这些现象对应到当前源码前,必须先分清实验开关:源码中存在字段或函数,不代表默认构建会执行它。 实验(GOEXPERIMENT值) Go 1.26.2默认值 本文中的影响 GreenTeaGC(greenteagc) 启用 部分小对象 span 使用页内 mark/scan bits SizeSpecializedMalloc(sizespecializedmalloc) 禁用 默认仍从通用 mallocgc 分派 RuntimeFreegc(runtimefreegc) 禁用 mcache.reusableNoscan 默认不参与分配 默认值以 internal/buildcfg 的实验 baseline 为准;后文提到关闭的实验路径时会再次标明。 先确认对象是否真的进入堆 new(T) 不等于堆分配,返回局部变量地址也不必机械地等于堆分配;编译器根据值是否可能在当前栈帧结束后继续可达、地址是否暴露给未知调用等条件做逃逸分析,随后内联和标量替换还可能消除分配。 GOpackage alloc type Node struct { next *Node val int } //go:noinline func makeNode(v int) *Node { return &Node{val: v} } BASHgo test -gcflags='all=-m=2' ./... go test -run='^$' -bench=. -benchmem ./... 第一条解释编译器决定,第二条测量最终二进制的每次操作分配。只看源码语法或只看逃逸日志都不足以判断生产路径成本。 ...

August 18, 2026