Golang内存模型
堆heap是Go运行时最大的临界资源。对操作系统而言,这是用户进程中缓存的内存;对于Go进程内部,堆是所有对象的内存起源。Golang在堆mheap上进行了细粒度化,建立了mcentral、mcache的模型。
- mheap:全局的内存起源,访问要加全局锁
- mcentral:每种对象大小规格对应的缓存,锁的粒度对应于一种规格
- mcache:每个P持有一份内存缓存,访问时不加锁
mspan(内存管理单元)
page是最小的存储单元,大小为8KB;mspan是最小的管理单元。mspan大小为page的整数倍,且从8KB到80KB被划分为67种不同的规格,分配对象时,会根据大小映射到不同规格的mspan,从中获取空间。根据规格的大小,产生了不同的等级。
具体大小为KB
- 宏观上提高了整体空间利用率
- 因为规格等级的概念,才支持mcentral实现细琐化
- mspan是Golang内存管理的最小单元
- mspan大小是页的整数倍(Go中的页大小为8KB)
- 每个mspan根据空间大小以及面向分配对象的大小,会被划分为不同的等级
- 同等级的mspan会从属于同一个mcentral,最终会被组织成链表
- 由于同等级的mspan内聚于同一个mcentral,所以会基于同一把互斥锁管理
- mspan会基于bitMap辅助快速找到空闲内存块(块大小为对应等级下的对象大小),此时需要使用到Ctz64算法
type mspan struct {
// 标识前后节点的指针
next *mspan
prev *mspan
// ...
// 起始地址
startAddr uintptr
// 包含几页,页是连续的
npages uintptr
// 标识此前的位置都已被占用
freeindex uintptr
// 最多可以存放多少个 object
nelems uintptr // number of object in the span.
// bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
allocCache uint64
// ...
// 标识 mspan 等级,包含 class 和 noscan 两部分信息
spanclass spanClass
// ...
}
内存管理组件
heapArena
heapArena是mheap向操作系统申请内存的单位(64MB)。每个heapArena包含8192(2的13次方)个页,大小为8192 * 8KB = 64MB。heapArena记录了页到mspan的映射。GC时,通过地址偏移找到页很方便,但找到其所属的mspan不容易,因此需要通过这个映射信息进行辅助。
spanClass(内存单元等级)
mspan根据空间大小和面向分配对象的大小,被划分为67种等级(1-67,第0级用于处理更大的对象)
mcache(线程缓存)
mcache是每个p独有的缓存,本地缓存可用的mspan资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源,无需加锁。他将每种spanClass等级的mspan各缓存了一个,总数为2(nocan维度) * 68(大小维度) = 136。
mcache在初始化的时候是没有任何mspan资源的,在使用过程中会动态地从mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache的相应规格的mspan进行分配。
mcentral(中心缓存)
每个mcentral对应一种spanClass,其下聚合了该spanClass下的mspan。mcentral下的mspan分为两个链表,分别为空闲链表partial span list和已分配链表full span list。每个mcentral需要加一把锁
type mcentral struct {
// 对应的 spanClass
spanclass spanClass
// 有空位的 mspan 集合,数组长度为 2 是用于抗一轮 GC
partial [2]spanSet
// 无空位的 mspan 集合
full [2]spanSet
}
mheap(全局堆缓存)
对于Golang上层应用而言,堆是操作系统虚拟内存的抽象,以8KB为单位作为最小存储单元。负责将连续页组装成mspan。全局内存基于bitMap标识其使用情况,每个bit对应一页,0表示空闲,1表示已被mspan组装。通过heapArena聚合页,记录了页到mspan的映射信息。建立空闲页基数索引radix tree index,辅助快速寻找空闲页。是mcentral的持有者,持有所有spanClass下的mcentral,作为自身的缓存。当内存不够时,向操作系统申请,申请单位为heapArena(64M)
当mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。
type mheap struct {
// 堆的全局锁
lock mutex
// 空闲页分配器,底层是多棵基数树组成的索引,每棵树对应 16 GB 内存空间
pages pageAlloc
// 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的
allspans []*mspan
// heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22]
// 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256T
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
// ...
// 多个 mcentral,总个数为 spanClass 的个数
central [numSpanClasses]struct {
mcentral mcentral
// 用于内存地址对齐
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
// ...
}
分配机制
Golang中,会依据object的大小,将其分为tiny(微对象)(0,16B),small(小对象)[16B,32KB],large(大对象)(32KB,无穷) 微对象的分配流程
- 从P专属mcache的tiny分配器取内存(无锁)
- 根据所属的spanClass,从P专属mcache缓存的mspan中取内存(无所)
- 根据所属的spanClass从对应的mcentral中取mspan填充到mcache,然后从mspan中取内存(spanClass粒度锁)
- 根据所属的spanClass,从mheap的页分配器pageAlloc取得足够数量空闲页组装成mspan填充到mcache,然后从mspan中取内存
- mheap向操作系统申请内存,更新页分配器的索引信息,然后重复(4) 小对象的分配流程:跳过1,执行2-5 大对象的分配流程:跳过1-3,执行4,5
mallocgc
new(T)
&T{}
make(xxxx)
无论以哪种方式,最终都会实现mallocgc方法
tiny分配
每个P独有的mcache拥有一个微对象分配器,每16B成块,基于offset线性移动的方式对微对象进行分配。对象会根据自身大小向上取2的整数倍字节进行空间补齐。
mcache分配
在mspan中,基于Ctz64算法,根据mspan.allocCache的bitMap信息快速检索到空闲的object块,进行返回。
mcentral分配
倘若mcache中对应的mspan空间不足,则会在mcache.refill方法中,向更上层的mcentral乃至mheap获取mspan,填充到mcache中。mcentral.cacheSpan方法中,分别从partial和full中尝试获取有空间的mspan。
mheap分配
倘若从partial和full中都找不到合适的mspan,则会调用mcentral的grow方法,经由mcentral.grow方法和mheap.alloc方法的周转,最终会步入mheap.allocSpan方法中
向操作系统申请
如果mheap中没有足够的空闲页,会发起mmap系统调用,向操作系统申请额外的内存空间