Go语言源码剖析——内存管理概览
模块概述
职责定义
Go的内存管理系统负责为程序分配和回收内存,是运行时系统的核心组件之一。内存分配器基于TCMalloc设计,采用多级缓存策略实现高效的内存分配。其核心目标是:快速分配、减少碎片、支持并发、与垃圾回收器协同工作。
设计理念
分级缓存架构
- mcache(P级缓存):每个P独占,无锁分配
- mcentral(中心缓存):按size class管理,所有P共享
- mheap(堆):全局页分配器,管理所有内存
尺寸分类(Size Class)
- 将对象大小分为约70个类别
- 同一类别的对象使用同一个span
- 减少内存碎片,提高分配效率
内存布局
- 以页(8KB)为基本单位
- Span:连续的页组成的内存块
- Arena:64MB的大块内存,包含多个span
输入与输出
输入
- 对象分配请求:
new、make、字面量创建 - 大小分类:tiny(<16B无指针)、小对象(≤32KB)、大对象(>32KB)
- GC标记信息:对象可达性、指针位置
- 操作系统内存:通过mmap/VirtualAlloc申请
输出
- 内存地址:返回已分配内存的指针
- 内存统计:堆大小、分配量、GC指标
- 内存释放:将未使用的span归还给OS
- 写屏障支持:配合GC的并发标记
上下游依赖
依赖的下游
- 操作系统:通过系统调用申请/释放内存
- P(Processor):每个P拥有独立的mcache
- 垃圾回收器:扫描堆内对象,标记存活对象
- 调度器:分配G的栈内存
被依赖的上游
- 所有Go代码:任何对象创建都需要内存分配
- Channel:channel结构和缓冲区需要分配
- Map:map的bucket需要分配
- Slice:底层数组需要分配
- Interface:iface结构需要分配
生命周期
初始化阶段(mallocinit)
- 确定地址空间布局
- 初始化mheap全局堆
- 为P创建mcache0(引导阶段)
- 设置size class和span class映射表
- 从OS申请初始arena
运行阶段
- 快速路径:从mcache直接分配(无锁)
- 慢路径:mcache缺失,从mcentral补充span
- 更慢路径:mcentral缺失,从mheap申请页
- 最慢路径:mheap不足,向OS申请新arena
- 清理回收:GC后归还未使用的内存给OS
模块架构图
flowchart TB
subgraph "P的mcache 内存分配缓存"
MCACHE[mcache结构]
TINY[tiny分配器<br/>微小对象<16B]
ALLOC[alloc数组<br/>136个span class]
STACK[stackcache<br/>栈缓存]
MCACHE --> TINY
MCACHE --> ALLOC
MCACHE --> STACK
end
subgraph "mcentral 中心缓存"
CENTRAL[mcentral数组<br/>按size class索引]
PARTIAL[partial链表<br/>部分空闲span]
FULL[full链表<br/>已满span]
CENTRAL --> PARTIAL
CENTRAL --> FULL
end
subgraph "mheap 全局堆"
HEAP[mheap结构]
PAGES[pages页分配器<br/>radix tree]
ARENAS[arenas数组<br/>管理64MB块]
SPANS[allspans<br/>所有span记录]
HEAP --> PAGES
HEAP --> ARENAS
HEAP --> SPANS
end
subgraph "span 内存块"
SPAN1[mspan<br/>8KB页]
SPAN2[mspan<br/>16KB页]
SPAN3[mspan<br/>32KB页]
BITMAP[allocBits<br/>分配位图]
SPAN1 --> BITMAP
SPAN2 --> BITMAP
SPAN3 --> BITMAP
end
subgraph "操作系统"
OS[OS内存<br/>mmap/VirtualAlloc]
end
ALLOC -.span耗尽.-> CENTRAL
CENTRAL -.span耗尽.-> HEAP
HEAP -.内存不足.-> OS
HEAP -.返还.-> OS
CENTRAL -.归还.-> HEAP
ALLOC -.归还.-> CENTRAL
subgraph "分配路径"
REQ[分配请求]
SIZECHECK{对象大小?}
TINYPATH[tiny路径<br/>≤16B无指针]
SMALLPATH[small路径<br/>≤32KB]
LARGEPATH[large路径<br/>>32KB]
REQ --> SIZECHECK
SIZECHECK -->|tiny| TINYPATH
SIZECHECK -->|small| SMALLPATH
SIZECHECK -->|large| LARGEPATH
TINYPATH --> TINY
SMALLPATH --> ALLOC
LARGEPATH --> HEAP
end
架构图说明
三级缓存结构
第一级:mcache(P级缓存)
- 每个P拥有独立的mcache
- 无锁快速分配
- 包含136个span(68个size class × 2种scan/noscan)
- 包含tiny分配器用于微小对象(<16B且无指针)
第二级:mcentral(中心缓存)
- 每个size class对应一个mcentral
- 管理两个链表:partial(部分空闲)和full(已满)
- P的mcache缺少span时,从mcentral补充
- 需要加锁,但粒度细(每个size class一个锁)
第三级:mheap(全局堆)
- 全局唯一的堆管理器
- 管理所有的页(page)和span
- mcentral缺少span时,从mheap申请
- 使用radix tree高效管理大量页
- 需要全局锁,访问频率最低
对象分类与分配策略
Tiny对象(<16B且无指针)
分配策略:
1. 多个tiny对象合并到一个16B块中
2. 减少内存碎片和分配次数
3. 从P的mcache.tiny分配
4. 例子:小整数、bool、小字符串
Small对象(16B~32KB)
分配策略:
1. 根据对象大小映射到67个size class之一
2. 从mcache对应span的分配位图查找空闲slot
3. span满时从mcentral获取新span
4. 例子:大部分Go对象、小slice、小map
Large对象(>32KB)
分配策略:
1. 直接从mheap分配
2. 不经过mcache和mcentral
3. 分配整数个页(8KB的倍数)
4. 例子:大数组、大slice、大map
Size Class映射表
Go定义了67个size class(实际使用中是68,因为class 0特殊):
| Class | Object Size | Span Size(pages) | Objects | Waste |
|---|---|---|---|---|
| 1 | 8B | 1 (8KB) | 1024 | 0% |
| 2 | 16B | 1 | 512 | 0% |
| 3 | 24B | 1 | 341 | 1.56% |
| 4 | 32B | 1 | 256 | 0% |
| … | … | … | … | … |
| 32 | 1024B | 1 | 8 | 0% |
| 33 | 1152B | 1 | 7 | 1.39% |
| … | … | … | … | … |
| 67 | 32768B | 4 (32KB) | 1 | 0% |
Span Class = Size Class × 2 + (noscan?1:0)
- 每个size class分为scan和noscan两种
- scan:包含指针,需要GC扫描
- noscan:不包含指针,GC可跳过
- 共136个span class(68个size × 2)
mspan数据结构
type mspan struct {
next *mspan // 链表指针
prev *mspan // 链表指针
startAddr uintptr // span起始地址
npages uintptr // span包含的页数
nelems uintptr // span中object数量
elemsize uintptr // object大小
spanclass spanClass // size class and noscan
allocCount uint16 // 已分配object数量
allocBits *gcBits // 分配位图
gcmarkBits *gcBits // GC标记位图
sweepgen uint32 // sweep generation
allocCache uint64 // allocBits的缓存
// 链表管理
state mSpanStateBox // mSpanInUse等
needzero uint8 // 是否需要清零
divMul uint32 // 用于快速除法
}
关键字段说明
startAddr:span的起始虚拟地址npages:span占用的8KB页数nelems:span可容纳的对象数量elemsize:每个对象的大小allocCount:已分配对象数allocBits:位图标记哪些slot已分配gcmarkBits:GC标记位图allocCache:allocBits的64位缓存,加速查找空闲slot
分配算法
mallocgc()核心流程
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 1. 快速路径:tiny对象
if size <= maxTinySize && typ.Kind_&kindNoPointers != 0 {
off := c.tinyoffset
// 对齐
if size&7 == 0 {
off = alignUp(off, 8)
} else if goarch.PtrSize == 4 && size == 12 {
off = alignUp(off, 8)
} else if size&3 == 0 {
off = alignUp(off, 4)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
if off+size <= maxTinySize && c.tiny != 0 {
// 有足够空间,直接分配
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
return x
}
// 分配新的tiny块
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
c.tiny = uintptr(x)
c.tinyoffset = size
return x
}
// 2. 小对象路径
if size <= maxSmallSize {
if typ.Kind_&kindNoPointers != 0 {
span = c.alloc[tSpanClass]
}
// 从span分配
v := nextFreeFast(span)
if v == 0 {
v, span = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size)
}
return x
}
// 3. 大对象路径
span = c.allocLarge(size, typ)
x = unsafe.Pointer(span.base())
return x
}
nextFreeFast()快速分配
func nextFreeFast(s *mspan) gclinkptr {
theBit := sys.TrailingZeros64(s.allocCache)
if theBit < 64 {
result := s.freeindex + uintptr(theBit)
if result < s.nelems {
freeidx := result + 1
if freeidx%64 == 0 && freeidx != s.nelems {
return 0 // 缓存用完,走慢路径
}
s.allocCache >>= uint(theBit + 1)
s.freeindex = freeidx
s.allocCount++
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
算法优化点
- 使用
allocCache缓存64个slot的状态,避免每次都读取内存 - 使用
TrailingZeros64指令快速找到第一个空闲slot - Tiny分配器减少小对象的内存开销和碎片
内存布局
虚拟地址空间布局(64位)
+------------------+
| Stack | 栈向下增长
+------------------+
| ↓ |
| |
| ↑ |
+------------------+
| Heap | 堆向上增长
| - arenas | 64MB对齐的大块
| - spans | 管理元数据
+------------------+
| .bss/.data | 全局变量
+------------------+
| .text | 代码段
+------------------+
Arena布局(64MB块)
每个arena = 64MB = 8192个page(8KB)
arena metadata存储在heapArena结构中:
- bitmap:标记哪些字包含指针
- spans:指向覆盖该区域的mspan
Span与Object布局
mspan结构 (metadata)
+------------------+
| startAddr | ------+
| npages | |
| elemsize | |
| allocBits | |
+------------------+ |
|
实际内存 |
+------------------+ <-----+
| object 0 |
+------------------+
| object 1 |
+------------------+
| object 2 |
+------------------+
| ... |
+------------------+
与GC的协同
写屏障支持
- 分配器需要支持写屏障的开启/关闭
mallocgc在GC标记阶段会触发标记辅助(mark assist)- 黑色对象分配:新分配的对象标记为黑色(已扫描)
标记位图
- 每个span有两个位图:allocBits和gcmarkBits
- allocBits:哪些object已分配
- gcmarkBits:GC标记哪些object存活
- GC sweep阶段,gcmarkBits变成新的allocBits
Sweep清扫
- GC标记完成后,清扫未标记的object
- 惰性清扫:span在下次分配时才清扫
- 清扫时释放未使用的span给mcentral
核心算法详解
1. 对象大小到Size Class的映射
// size到class的映射表
var class_to_size = [_NumSizeClasses]uint16{
0, 8, 16, 24, 32, 48, 64, 80,
96, 112, 128, 144, 160, 176, 192, 208,
224, 240, 256, 288, 320, 352, 384, 416,
448, 480, 512, 576, 640, 704, 768, 896,
1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304,
2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144,
6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880,
12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760,
24576, 27264, 28672, 32768,
}
// 快速查找size class
func sizeToClass(size uintptr) uint8 {
if size > maxSmallSize {
throw("invalid size")
}
if size > 1024-8 {
return uint8(size_to_class128[(size-1024+127)>>7])
}
return uint8(size_to_class8[(size-1+smallSizeDiv-1)/smallSizeDiv])
}
2. mcentral的span管理
type mcentral struct {
spanclass spanClass // size class
partial [2]spanSet // 部分空闲的span列表
full [2]spanSet // 已满的span列表
}
// 从mcentral获取span
func (c *mcentral) cacheSpan() *mspan {
sg := mheap_.sweepgen
retry:
// 1. 从partial列表获取
var s *mspan
if s = c.partial[sg%2].pop(); s != nil {
goto havespan
}
// 2. 从full列表获取(已清扫的span)
for s = c.full[sg%2].pop(); s != nil; s = c.full[sg%2].pop() {
if s.sweep(false) {
goto havespan
}
}
// 3. 从mheap申请新span
s = c.grow()
if s == nil {
return nil
}
havespan:
// 设置span为已缓存状态
n := int(s.nelems) - int(s.allocCount)
if n == 0 || s.freeindex == s.nelems {
throw("span has no free objects")
}
return s
}
3. mheap的页分配
type mheap struct {
lock mutex
pages pageAlloc // 页分配器
allspans []*mspan // 所有span
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
}
// 分配npages页
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
// 1. 从页分配器获取连续页
base, scav := h.pages.alloc(npages)
if base == 0 {
// 向OS申请更多内存
if !h.grow(npages) {
return nil
}
base, scav = h.pages.alloc(npages)
if base == 0 {
throw("grew heap, but no adequate free space found")
}
}
// 2. 初始化span
s := h.allocMSpanLocked()
s.init(base, npages)
s.spanclass = spanclass
// 3. 设置arena映射
h.setSpans(s.base(), npages, s)
return s
}
核心时序图
1. 小对象分配时序图(Tiny/Small Object)
sequenceDiagram
autonumber
participant User as 用户代码
participant Compiler as 编译器
participant G as Goroutine
participant P as P(处理器)
participant mcache as mcache<br/>(P的内存缓存)
participant mspan as mspan<br/>(当前span)
participant mcentral as mcentral<br/>(中央缓存)
User->>Compiler: obj := new(T)
Compiler->>G: mallocgc(size, typ, needzero)
alt size <= 16 byte (Tiny对象)
G->>P: 获取当前P
P->>mcache: 访问P.mcache
mcache->>mcache: 检查tiny块<br/>(tiny, tinyoffset)
alt tiny块有足够空间
mcache->>mcache: 从tiny块分配<br/>offset += size
mcache-->>G: 返回tiny+offset
else tiny块空间不足
mcache->>mspan: 访问tiny sizeclass的span
mspan->>mspan: nextFreeFast()<br/>查找空闲对象
mspan-->>mcache: 返回新tiny块
mcache->>mcache: 替换tiny块<br/>offset = size
mcache-->>G: 返回新地址
end
else 16 < size <= 32KB (Small对象)
G->>G: 计算sizeclass<br/>size → sizeclass
G->>mcache: 获取对应span<br/>alloc[sizeclass]
mcache->>mspan: 访问mspan
alt span有空闲对象
mspan->>mspan: nextFreeFast()<br/>快速路径
alt allocCache非零
mspan->>mspan: ctz(allocCache)<br/>找到bit位置
mspan->>mspan: 清除bit<br/>标记已分配
mspan-->>mcache: 返回对象地址
else allocCache为零
mspan->>mspan: nextFree()<br/>慢速路径
mspan->>mspan: 刷新allocCache<br/>从allocBits加载
mspan->>mspan: 再次ctz查找
mspan-->>mcache: 返回对象地址
end
mcache-->>G: 返回对象指针
else span已满
G->>mcentral: refill(sizeclass)
mcentral->>mcentral: cacheSpan()<br/>获取新span
alt nonempty列表有span
mcentral->>mcentral: 从nonempty获取
mcentral-->>mcache: 返回span
else 从empty列表获取
mcentral->>mcentral: sweep并获取
mcentral-->>mcache: 返回span
else 都无可用span
mcentral->>mcentral: grow(sizeclass)
mcentral->>mcentral: 向mheap申请<br/>alloc(npages)
mcentral->>mspan: 切分新span
mcentral-->>mcache: 返回新span
end
mcache->>mcache: 替换原span<br/>alloc[sizeclass] = newSpan
mcache->>mspan: nextFreeFast()
mspan-->>mcache: 返回对象地址
mcache-->>G: 返回对象指针
end
end
G->>G: 零值初始化(如需)
G-->>User: 返回对象指针
时序说明
-
步骤1-3:编译器转换与入口调用
- 编译器将
new(T)转换为runtime.newobject(typ) newobject内部调用mallocgc(typ.size, typ, true)true表示需要零值初始化
- 编译器将
-
步骤4-12:Tiny对象分配路径(≤16字节)
- Tiny块机制:mcache维护一个16字节的tiny块和offset
- 按8字节对齐将多个小对象合并到同一个tiny块
- 优势:减少内存碎片,提高缓存利用率
- 检查顺序:
- 当前tiny块是否有足够空间(
16 - tinyoffset >= size) - 若有,直接分配并更新offset
- 若无,从tiny sizeclass(通常是class 1)分配新16字节块
- 当前tiny块是否有足够空间(
- 对齐处理:根据对象大小调整offset(8字节对齐)
-
步骤13-26:Small对象分配快速路径(16B - 32KB)
- Sizeclass计算:通过查表将size映射到67个sizeclass之一
sizeclass := size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] - mcache查找:每个P的mcache有67*2=134个mspan槽位(scan/noscan)
nextFreeFast()快速路径:allocCache:64位bitmap缓存,每位表示一个对象的分配状态ctz(allocCache):count trailing zeros,找到第一个0位(空闲对象)- 时间复杂度:O(1),使用CPU的TZCNT指令
nextFree()慢速路径:- allocCache为全1时触发,从
allocBits加载下一个64位 - allocBits是完整的bitmap,记录span中所有对象状态
- allocCache为全1时触发,从
- Sizeclass计算:通过查表将size映射到67个sizeclass之一
-
步骤27-42:Span满时从mcentral refill
refill(sizeclass)流程:- 将当前满的span归还给mcentral的empty列表
- 从mcentral的nonempty列表获取新span
cacheSpan()查找策略:- nonempty列表:有空闲对象的span(优先)
- empty列表:通过sweep找到可回收对象
- grow:向mheap申请新内存并切分
- 加锁粒度:仅在访问mcentral时加锁,每个sizeclass有独立锁
-
步骤43-45:零值初始化与返回
- 根据
needzero参数决定是否清零 - 小对象通常清零(
new和make),编译器优化可能省略 - 返回对象地址,类型指针存储在span.elemtype
- 根据
边界与性能
- Tiny对象开销:~10-20ns(无竞争,缓存命中)
- Small对象开销:
- 快速路径:~20-50ns(mcache命中)
- 慢速路径:~200-500ns(mcentral refill)
- 最慢路径:~5-20µs(mheap grow + 切分)
- 并发性:mcache无锁,每个P独立分配
- 碎片率:tiny对象合并减少碎片到<5%
异常与回退
- OOM处理:mheap无法分配时触发
runtime.throw("out of memory") - GC触发:分配达到阈值时触发GC(
gcTrigger.test()) - 自动扩容:
sysReserve动态向OS申请内存
2. 大对象分配时序图(Large Object >32KB)
sequenceDiagram
autonumber
participant User as 用户代码
participant G as Goroutine
participant mheap as mheap<br/>(全局堆)
participant PageCache as PageCache<br/>(页缓存)
participant Arena as Arena<br/>(内存区域)
participant OS as 操作系统
User->>G: obj := make([]T, largeN)
G->>G: mallocgc(size, typ, false)
G->>G: 判断size > 32KB
G->>mheap: alloc(npages, sizeclass=0)
mheap->>mheap: 加全局锁<br/>lock(&mheap.lock)
alt 1. 尝试PageCache
mheap->>PageCache: 查找npages页<br/>(P本地页缓存)
alt PageCache命中
PageCache->>PageCache: 取出连续页
PageCache-->>mheap: 返回页地址
mheap->>mheap: 初始化span<br/>(sizeclass=0, npages)
mheap-->>mheap: 解锁
mheap-->>G: 返回span
end
end
alt 2. 尝试free tree查找
mheap->>mheap: 查找free tree<br/>bestFit(npages)
alt 找到合适span
mheap->>mheap: 从tree移除span
alt span大小匹配
mheap->>mheap: 直接使用
mheap-->>mheap: 解锁
mheap-->>G: 返回span
else span过大
mheap->>mheap: 切分span<br/>split(span, npages)
mheap->>mheap: 剩余部分放回tree
mheap-->>mheap: 解锁
mheap-->>G: 返回span
end
end
end
alt 3. 从Arena申请新内存
mheap->>mheap: grow(npages)
mheap->>Arena: sysReserve(size)
Arena->>OS: mmap(PROT_NONE)
OS-->>Arena: 返回虚拟地址
Arena->>OS: madvise(MADV_NOHUGEPAGE)
OS-->>Arena: 配置页表
Arena-->>mheap: 返回新区域
mheap->>mheap: 创建新span
mheap->>mheap: 更新arena映射<br/>setSpans()
mheap-->>mheap: 解锁
mheap-->>G: 返回新span
end
G->>G: 设置span.sizeclass=0<br/>span.elemsize=size
G-->>User: 返回对象指针
时序说明
-
步骤1-4:大对象判定与入口
- 大对象阈值:size > 32KB
- 跳过mcache和mcentral,直接从mheap分配
- 计算所需页数:
npages = (size + pageSize - 1) / pageSize sizeclass=0标识大对象
-
步骤5-12:尝试从PageCache快速分配
- PageCache:每个P维护的页缓存,存储最近释放的页
- 缓存大小:通常64KB,约8页
- 无锁访问:仅访问P本地数据,无竞争
- 命中优势:避免全局mheap.lock竞争
-
步骤13-26:从free tree查找合适span
- Treap结构:mheap维护的空闲span索引树
- 按span大小组织(heap性质)
- 按地址哈希随机化(tree性质)
- 支持O(log n)查找和插入
- bestFit查找:
- 查找≥npages的最小span
- 减少内存浪费
- 切分逻辑:
- 若找到的span过大,切分为两部分
- 使用前npages,剩余部分放回tree
- 切分后的span独立管理,GC单独标记
- Treap结构:mheap维护的空闲span索引树
-
步骤27-40:从OS申请新内存(grow)
grow(npages)触发条件:- PageCache和free tree都无可用内存
- 向OS申请至少1MB(或npages * pageSize,取较大者)
sysReserve()系统调用:- Linux:
mmap(PROT_NONE)预留地址空间 - 延迟物理内存分配(lazy commit)
- 避免透明大页(THP)影响性能
- Linux:
- arena映射更新:
h.arenas记录所有已分配区域h.spans维护地址到span的映射setSpans(base, npages, s)建立索引
-
步骤41-42:初始化span元数据
span.sizeclass = 0:标识大对象span.elemsize = size:记录对象实际大小span.nelems = 1:大对象独占spanspan.allocCount = 1:已分配计数
边界与性能
- 分配开销:
- PageCache命中:~100-200ns(无锁)
- Free tree查找:~500ns-2µs(全局锁竞争)
- OS grow:~10-50µs(系统调用 + 页表设置)
- 锁竞争:mheap.lock是全局锁,高并发时可能成为瓶颈
- 内存利用率:bestFit算法减少外部碎片
- 虚拟内存:预留但未commit,减少物理内存压力
异常与回退
- OOM:
sysReserve失败时抛出panic - 碎片整理:GC sweep阶段合并相邻空闲span
- 地址空间耗尽:64位系统几乎不会发生,32位需注意
3. mcentral refill与sweep时序图
sequenceDiagram
autonumber
participant mcache as mcache<br/>(P的缓存)
participant mcentral as mcentral<br/>(中央缓存)
participant nonempty as nonempty<br/>列表
participant empty as empty<br/>列表
participant span1 as Span1<br/>(被sweep)
participant mheap as mheap<br/>(全局堆)
mcache->>mcentral: refill(sizeclass)
mcentral->>mcentral: lock(&c.lock)
alt 1. 从nonempty获取
mcentral->>nonempty: 遍历nonempty列表
alt 找到有空闲对象的span
nonempty->>mcentral: 返回span
mcentral->>mcentral: 从列表移除
mcentral-->>mcentral: unlock
mcentral-->>mcache: 返回span
end
end
alt 2. 从empty列表sweep
mcentral->>empty: 遍历empty列表
loop 扫描empty列表
mcentral->>span1: 检查span状态
alt span未sweep
mcentral->>span1: sweep(s, true)
span1->>span1: 扫描allocBits<br/>查找可回收对象
alt 找到可回收对象
span1->>span1: 重置allocCount<br/>刷新allocCache
span1-->>mcentral: 返回span
mcentral->>mcentral: 从empty移除
mcentral-->>mcentral: unlock
mcentral-->>mcache: 返回span
else 完全空闲
span1-->>mcentral: 标记可释放
mcentral->>mcentral: 继续扫描
else 完全满
mcentral->>mcentral: 保留在empty
end
end
end
end
alt 3. 向mheap申请新span
mcentral->>mcentral: grow(sizeclass)
mcentral->>mcentral: unlock暂时释放锁
mcentral->>mheap: alloc(npages)
mheap->>mheap: 分配连续页<br/>(前述大对象流程)
mheap-->>mcentral: 返回新span
mcentral->>mcentral: lock重新获取锁
mcentral->>span1: 初始化新span
span1->>span1: 设置sizeclass<br/>nelems, elemsize
span1->>span1: 初始化allocBits<br/>全0(全空闲)
mcentral->>nonempty: 将新span加入nonempty
mcentral-->>mcentral: unlock
mcentral-->>mcache: 返回新span
end
时序说明
-
步骤1-9:从nonempty列表快速获取
- nonempty列表:包含至少一个空闲对象的span
- 遍历策略:从列表头开始,取第一个可用span
- 移除逻辑:span被mcache获取后从列表移除
- 归还时机:mcache释放span时,根据空闲对象数量决定放入nonempty或empty
-
步骤10-28:从empty列表sweep回收
- empty列表:可能完全满或等待sweep的span
- Sweep过程:
- 扫描
allocBits和gcmarkBits对比 - GC标记过但未在allocBits的对象可回收
- 更新
allocCount(已分配对象数) - 刷新
allocCache为span前64个对象的状态
- 扫描
- 三种结果:
- 有空闲对象:移到nonempty并返回
- 完全空闲:归还给mheap(
freeSpan) - 完全满:保留在empty,等待未来回收
-
步骤29-44:向mheap申请新span
grow(sizeclass)触发:nonempty和empty都无可用span- 页数计算:根据sizeclass决定span大小
npages := class_to_allocnpages[sizeclass] - 临时释放锁:调用mheap.alloc时释放mcentral锁,避免长时间持锁
- 初始化新span:
- 设置
span.sizeclass,span.nelems,span.elemsize - 初始化
allocBits为全0(所有对象空闲) - 计算
allocCache为前64位的镜像
- 设置
- 加入nonempty:新span有满span的空闲对象,放入nonempty列表
边界与性能
- 锁粒度:每个sizeclass独立的mcentral.lock,减少竞争
- Sweep延迟:lazy sweep策略,仅在需要时sweep
- 批量操作:一次refill返回一个完整span(约几十到几百个对象)
- 缓存效果:mcache缓存减少refill频率
异常与回退
- Sweep失败:所有span都满时触发grow
- 内存压力:GC触发更积极的sweep
- Span合并:完全空闲的span归还mheap,可能与相邻span合并
性能优化要点
- 双列表设计:nonempty和empty分离,加速查找
- Lazy sweep:延迟清理到实际需要时,分摊开销
- 批量refill:一次获取整个span,减少mcentral访问次数
性能优化要点
1. 无锁快速路径
- mcache属于P,分配时无需加锁
nextFreeFast使用CPU的TrailingZeros指令- 使用
allocCache缓存避免内存访问
2. 批量操作
- mcache从mcentral获取span时,一次获取整个span
- mcentral从mheap获取页时,一次分配多个页
- 减少锁的获取次数
3. NUMA感知
- 尝试从本地NUMA节点分配内存
- 减少跨节点内存访问延迟
4. 内存复用
- G结构、defer结构、sudog结构都使用缓存池
- span在释放后加入free列表,避免重新分配
5. Huge Page支持
- 在支持的平台使用透明大页(Transparent Huge Pages)
- 减少TLB miss
最佳实践
1. 减少小对象分配
// 不推荐:频繁分配小对象
for i := 0; i < 1000000; i++ {
s := fmt.Sprintf("%d", i) // 每次分配新字符串
process(s)
}
// 推荐:复用buffer
var buf bytes.Buffer
for i := 0; i < 1000000; i++ {
buf.Reset()
fmt.Fprintf(&buf, "%d", i)
process(buf.String())
}
2. 使用sync.Pool
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process() {
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
// 使用buf
}
3. 预分配slice
// 不推荐:append导致多次分配
var data []int
for i := 0; i < 10000; i++ {
data = append(data, i)
}
// 推荐:预分配容量
data := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
data = append(data, i)
}