Go运行时内存管理
Go运行时采用分层内存管理,高效分配和复用内存。
内存分配层级
mcache → mcentral → mheap
Go
Goroutine申请内存
↓
mcache(本地缓存)← 无锁分配
↓ 缓存不足
mcentral(中央缓存)← 有锁获取
↓ 不足
mheap(堆)← 系统申请
↓
操作系统(mmap)
mcache实现无锁分配,小对象分配极其高效。
mcache本地缓存
结构与作用
Go
// 每个P拥有一个mcache
type mcache struct {
// 按大小级别缓存Span
alloc [numSpanClasses]*mspan
// 微对象分配器
tiny uintptr
tinyoffset uintptr
}
// 无锁分配小对象
func allocSmall(size) {
span := mcache.alloc[sizeClass]
return span.alloc()
}
Span级别划分
Go
对象大小 → Span级别(size class)
1-8字节 → class 1(8字节)
9-16字节 → class 2(16字节)
17-32字节 → class 3(32字节)
...
32768字节 → class 67
共67个级别,大对象直接从堆分配
小于32KB的对象从Span级别分配,大于32KB直接从堆分配。
mcentral中央缓存
结构与作用
Go
type mcentral struct {
sizeclass int // 大小级别
nonempty mSpanList // 有空闲Span列表
empty mSpanList // 无空闲Span列表
}
// mcache不足时从mcentral获取
func cacheSpan() *mspan {
// 从nonempty获取
// 或从heap申请新Span
}
Span流转
Go
mcentral.nonempty(有空闲)
↓ 分配给mcache
mcentral.empty(无空闲)
↓ 回收后放回nonempty
mheap堆管理
结构与作用
Go
type mheap struct {
arenas []arena // 堆区域数组
central [numSpanClasses]mcentral
// Arena结构
// 64MB(Linux)或4MB(Windows)
}
// 大对象或mcentral不足时
func allocSpan(size) *mspan {
// 从空闲Arena分配
// 或向系统申请新Arena
}
Arena划分
Go
堆由多个Arena组成
Linux:每个Arena 64MB
Windows:每个Arena 4MB
Arena进一步划分为Span
Span管理同大小对象
Span内存单元
Span结构
Go
type mspan struct {
startAddr uintptr // 起始地址
npages uintptr // 页数
nelems uintptr // 对象数量
allocBits *gcBits // 分配位图
freeindex uintptr // 下一个空闲索引
}
// Span包含多个相同大小对象
// 通过位图标记已分配/空闲
Span分配对象
Go
// 从Span分配一个对象
func span.alloc() uintptr {
// 查找allocBits空闲位
index := freeindex
// 计算地址
addr := startAddr + index * elemsize
// 更新位图
allocBits.set(index)
return addr
}
内存分配流程表
| 步骤 | 层级 | 锁竞争 | 对象大小 |
|---|---|---|---|
| 1 | mcache | 无锁 | <32KB |
| 2 | mcentral | 有锁 | mcache不足 |
| 3 | mheap | 全局锁 | >32KB或不足 |
| 4 | OS | 系统调用 | 堆不足 |
对象大小分类
| 类别 | 大小 | 分配位置 |
|---|---|---|
| 微对象 | <16字节 | tiny allocator |
| 小对象 | 16-32KB | mcache→Span |
| 大对象 | >32KB | mheap直接 |
tiny allocator
微对象分配
Go
// 极小对象(<16字节)使用tiny分配
type mcache struct {
tiny uintptr // 当前tiny块地址
tinyoffset uintptr // tiny块内偏移
}
// 合多个小对象到同一16字节块
func allocTiny(size) {
if tinyoffset + size <= 16 {
// 从当前块分配
tinyoffset += size
return tiny + tinyoffset
}
// 申请新tiny块
}
tiny allocator合并多个极小对象,减少内存碎片。
内存复用机制
Span回收
Bash
// Span对象全部释放后
func sweepSpan(span) {
if span.allocCount == 0 {
// 放回mcentral
mcentral.nonempty.insert(span)
}
}
Span迁移
text
全部空闲 → 放回mcentral.nonempty
部分空闲 → 保留在mcache
完全满 → 等待sweep回收
内存统计
runtime.ReadMemStats
text
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println("Alloc:", m.Alloc) // 当前分配
fmt.Println("TotalAlloc:", m.TotalAlloc) // 累计分配
fmt.Println("Sys:", m.Sys) // 系统申请
fmt.Println("HeapAlloc:", m.HeapAlloc) // 堆分配
fmt.Println("HeapSys:", m.HeapSys) // 堆系统内存
MemStats主要字段
| 字段 | 含义 |
|---|---|
| Alloc | 当前使用内存 |
| TotalAlloc | 累计分配内存 |
| Sys | 从OS申请内存 |
| HeapAlloc | 堆分配量 |
| HeapSys | 申请系统内存 |
| HeapIdle | 堆空闲内存 |
| HeapInuse | 堆使用内存 |
| HeapReleased | 释放给OS的堆内存 |
| HeapObjects | 堆对象数量 |
内存碎片控制
tcmalloc思想
text
// Go借鉴tcmalloc设计
// 1. 分级Span减少碎片
// 2. 线程本地缓存减少锁
// 3. Span复用减少分配
// Span大小与对象大小匹配
// 8字节对象 → 8KB Span(1024个对象)
// 16字节对象 → 16KB Span(1024个对象)
碎片率控制
text
理论碎片率:
- 小对象:<10%
- 大对象:低碎片
实际碎片率:
- Span内碎片(最后一个对象未满)
- Span间碎片(部分空闲Span)
内存释放
heap释放
text
// 空闲内存归还OS
func scavenge() {
if heapIdle > threshold {
// madvise(DONTNEED)
// 或munmap
heapReleased += size
}
}
内存延迟释放
text
GC后内存不立即归还OS
↓ 等待一段时间
scavenge周期检查
↓ 空闲超过阈值
释放给操作系统
默认延迟释放,避免频繁系统调用。
内存分配器调试
GODEBUG选项
text
# 查看内存分配器信息
GODEBUG=allocfreetrace=1
# 查看内存统计
GODEBUG=memprofile=mem.prof
# 运行内存分析
go tool trace mem.prof
要点总结
- mcache本地缓存实现无锁分配
- mcentral中央缓存补充mcache
- mheap管理全局堆内存
- Span是内存分配单元
- 小对象(<32KB)走Span级别
- 大对象(>32KB)直接从堆分配
- tiny allocator处理极小对象
- 分级管理减少内存碎片
- ReadMemStats查看内存统计
- 空闲内存延迟归还OS
- 借鉴tcmalloc高效分配思想
📝 发现内容有误?点击此处直接编辑