Go 内存分配器的设计与实现( 五 )


表 7-3 跨度类的数据
上表展示了对象大小从 8B 到 32KB,总共 66 种跨度类的大小、存储的对象数以及浪费的内存空间,以表中的第四个跨度类为例,跨度类为 4 的 runtime.mspan 中对象的大小上限为 48 字节、管理 1 个页、最多可以存储 170 个对象 。因为内存需要按照页进行管理,所以在尾部会浪费 32 字节的内存,当页中存储的对象都是 33 字节时,最多会浪费 31.52% 的资源:

Go 内存分配器的设计与实现

文章插图
 
mspan-max-waste-memory
图 7-14 跨度类浪费的内存
除了上述 66 个跨度类之外,运行时中还包含 ID 为 0 的特殊跨度类,它能够管理大于 32KB 的特殊对象,我们会在后面详细介绍大对象的分配过程,在这里就不展开说明了 。
跨度类中除了存储类别的 ID 之外,它还会存储一个 noscan 标记位,该标记位表示对象是否包含指针,垃圾回收会对包含指针的 runtime.mspan 结构体进行扫描 。我们可以通过下面的几个函数和方法了解 ID 和标记位的底层存储方式:
func makeSpanClass(sizeclass uint8, noscan bool) spanClass { return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))}func (sc spanClass) sizeclass() int8 { return int8(sc >> 1)}func (sc spanClass) noscan() bool { return sc&1 != 0}runtime.spanClass 是一个 uint8 类型的整数,它的前 7 位存储着跨度类的 ID,最后一位表示是否包含指针,该类型提供的两个方法能够帮我们快速获取对应的字段 。
线程缓存runtime.mcache 是 Go 语言中的线程缓存,它会与线程上的处理器一一绑定,主要用来缓存用户程序申请的微小对象 。每一个线程缓存都持有 67 * 2 个 runtime.mspan,这些内存管理单元都存储在结构体的 alloc 字段中:
Go 内存分配器的设计与实现

文章插图
 
mcache-and-mspans
图 7-15 线程缓存与内存管理单元
线程缓存在刚刚被初始化时是不包含 runtime.mspan 的,只有当用户程序申请内存时才会从上一级组件获取新的 runtime.mspan 满足内存分配的需求 。
初始化运行时在初始化处理器时会调用 runtime.allocmcache 初始化线程缓存,该函数会在系统栈中使用 runtime.mheap 中的线程缓存分配器初始化新的 runtime.mcache 结构体:
func allocmcache() *mcache { var c *mcache systemstack(func() {lock(&mheap_.lock)c = (*mcache)(mheap_.cachealloc.alloc())c.flushGen = mheap_.sweepgenunlock(&mheap_.lock) }) for i := range c.alloc {c.alloc[i] = &emptymspan } return c}就像我们在上面提到的,初始化后的 runtime.mcache 中的所有 runtime.mspan 都是空的占位符 emptymspan 。
替换runtime.mcache.refill 方法会为线程缓存获取一个指定跨度类的内存管理单元,被替换的单元不能包含空闲的内存空间,而获取的单元中需要至少包含一个空闲对象用于分配内存:
func (c *mcache) refill(spc spanClass) { s := c.alloc[spc] s = mheap_.central[spc].mcentral.cacheSpan() c.alloc[spc] = s}如上述代码所示,该函数会从中心缓存中申请新的 runtime.mspan 存储到线程缓存中,这也是向线程缓存中插入内存管理单元的唯一方法 。
微分配器线程缓存中还包含几个用于分配微对象的字段,下面的这三个字段组成了微对象分配器,专门为 16 字节以下的对象申请和管理内存:
type mcache struct { tinyuintptr tinyoffsetuintptr local_tinyallocs uintptr}微分配器只会用于分配非指针类型的内存,上述三个字段中 tiny 会指向堆中的一篇内存,tinyOffset 是下一个空闲内存所在的偏移量,最后的 local_tinyallocs 会记录内存分配器中分配的对象个数 。
中心缓存runtime.mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的内存管理单元需要使用互斥锁:
type mcentral struct { lockmutex spanclass spanClass nonemptymSpanList emptymSpanList nmalloc uint64}每一个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个runtime.mSpanList,分别存储包含空闲对象的列表和不包含空闲对象的链表:
Go 内存分配器的设计与实现

文章插图
 
mcentral-and-mspans
图 7-16 中心缓存和内存管理单元
该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表,nmalloc 字段也记录了该结构体中分配的对象个数 。
内存管理单元线程缓存会通过中心缓存的
runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:


推荐阅读