1. 内存分配器
Q: Go 内存管理的整体思路?
Go 采用分级分配的策略,按照不同大小的对象和不同内存层级来分配管理内存。
内存管理层级:
mspan → mcache → mcentral → mheap → heapArena分配流程:
每个 P 的 mcache 缓存每种规格的一个mspan(无锁访问)
↓
mcentral(8字节规格) → mspan链表 [8B][8B][8B]...
mcentral(16字节规格)→ mspan链表 [16B][16B][16B]...
mcentral(32字节规格)→ mspan链表 [32B][32B][32B]...
↓
mheap(全局堆)
↓
OS分配策略:
- 小于16B的对象:使用mcache微对象进行分配
- 16B-32KB的对象:首先计算出需要使用的span大小规格,然后使用mcache中相同大小规格的mspan分配
- 如果mcache没有可用的mspan:就会向mcentral申请
- 如果mcentral中没有可用的mspan:就向mheap申请
- 如果mheap中没有可用的span:就会向操作系统申请一系列新的页(最小1MB)
- 大于32KB的大对象:直接从mheap分配
Q: mspan 是什么?
mspan是golang内存分配管理的基本单位,是一个双向链表。
type mspan struct {
next *mspan // 链表指针
prev *mspan
startAddr uintptr // 管理的内存起始地址
npages uintptr // 占用的 page 数量(1 page = 8KB)
spanclass spanClass // 规格类别(如:8字节、16字节...)
elemsize uintptr // 每个 object 的大小
nelems uint16 // 总共有多少个 object
allocCount uint16 // 已分配多少个
freeindex uint16 // 下一个空闲索引
allocBits *gcBits // 分配位图
allocCache uint64 // 位图缓存
}基本特点:
- mspan共有8B-80KB(67种不同规格),其中class=0代表上不封顶,用于大对象分配
- 同样规格的mspan才可以连接在一起
mspan的优势:
消除外部碎片:传统内存分配器(如C的malloc)频繁分配释放不同大小的对象后,会产生大量不连续的小空闲内存——这就是外部碎片。mspan通过固定规格切割解决这个问题:每个mspan只存放一种固定大小的对象,分配时只要任意一个空槽就行,不要求连续空间
优化GC性能:可以通过spanclass快速判断,如果这个span是noscan类型(里面全是非指针对象),可以跳过扫描,提升GC效率
快速分配状态管理:mspan使用位图记录object分配状态,可以快速知道哪些分配了哪些没有分配
Q: mcache 是什么?
type mcache struct {
alloc [numSpanClasses]*mspan
}基本介绍:mcache内部维护了不同规格的mspan,每个Processor(P)都有一个mcache。
优势:
- 避免锁竞争:goroutine运行时会绑定到一个P上,每个P都有自己的mcache,goroutine在分配内存时只会访问自己P的mcache,避免了跨P的锁竞争
- 高并发优势:多个goroutine可以同时从自己的mcache获取内存,提升性能
Q: mcentral 是什么?
type mcentral struct {
lock mutex // 锁,由于每个P关联的mcache都可能会向mcentral申请空闲的span
spanclass spanClass // mcentral负责的span规格
nonempty mSpanList // 已经使用的span列表(链表)
empty mSpanList // 空闲span列表(链表)
nmalloc uint64 // mcentral已分配的span计数
}基本介绍:一个mcentral维护一个规格的mspan,当mcache中没有可用的mspan时就会向mcentral申请,不同的P可能会向同一个mcentral申请内存,所以需要加锁。
分配流程:
- 查看empty列表:如果有空闲的列表,就会直接把这个mspan分配给goroutine
- 如果没有空闲列表,查看nonempty列表:这里的是部分使用的,有些mspan已经被分配出去了,但还会有空闲的
- 回收空闲内存单元:然后把这些空闲的放到empty的列表里面,然后进行分配
- 如果mcentral不能满足分配要求:就会向mheap去申请新的内存页
Q: 内存逃逸是什么?
逃逸场景:
- 返回局部变量指针
- interface类型
- 闭包引用外部变量
- 切片/map动态扩容
- 大对象
影响:因为堆对象需要垃圾回收机制来释放内存,栈对象会随着函数结束被回收,所以大量的内存逃逸会给GC带来压力。
// 逃逸示例
func escape() *int {
x := 10
return &x // x逃逸到堆上
}
func noEscape() int {
x := 10
return x // x在栈上分配
}优化建议:通过
go build -gcflags="-m" 可以查看逃逸分析结果,帮助优化内存分配。