Golang内存模型与源码剖析
0、弁言本篇笔记用于记载作者在学习Golang的GC模型之前,对Golang内存模型的学习。目前使用的Go版本为1.22.4
1、Golang内存管理宏观布局
假设我们每次向内存池申请空间时,都必要频繁地向操纵系统发出哀求,这不仅会增长内存分配的时间,还可能引入竞争和锁的开销,从而导致性能瓶颈。尤其是在多线程并发的开发场景下,这样的题目带来的消耗是显而易见的。为了淘汰这种开销,我们不妨在初始化内存空间时,一次性地向操纵系统申请多一点的空间,只有当现有空间不足的时候,再次向操纵系统申请新的空间。通过这种方式,可以有效淘汰内存分配过程中多线程并发带来的竞争,提高步伐的性能。Go语言的内存管理模型正是围绕着高效的内存分配机制和垃圾接纳机制来优化这类题目标,从而在大规模并发应用中取得更好的性能表现。
Golang的内存管理布局宏观图如下:
https://img2024.cnblogs.com/blog/3542244/202412/3542244-20241219221208921-2137804841.png
设计到的核心数据布局有:
[*]mheap:Golang内存模型中最大的内存池,是全局的内存起源,它直接和操纵系统举行内存申请交互,向mheap申请内存必要持有锁。
[*]mcentral:mheap的粒度细化的内存池,存在于mheap中,总数量为136(Span Class)个。
[*]mcache:每个P持有的一份本地内存缓存,访问其不必要持有锁。
接下来我们来详细了解必要接触到的相关概念。
2、内存管理模型相关概念及源码剖析
2.1、page
借鉴操纵系统内存分页管理的思想,Golang的内存管理模型也存在着Page,其是内存管理模型和操纵系统内存交互的最小单元,大小为8KB,对于Golang来说,操纵系统的假造内存就是被划分成N个Page的大内存池。
2.2、mspan
多个连续的page被称之为mspan,其大小为8KB~32KB。其根据分配object大小来划分可以划分为67种。
其源码的核心字段如下:
type mspan struct {
//标识前后mspan的指针
next *mspan
prev *mspan
//起始地址
startAddr uintptr
//包含的页数
npages uintptr
// freeindex 是一个槽索引,范围在 0 到 nelems 之间,表示开始扫描该 span 中下一个空闲对象的位置。
// 每次分配都会从 freeindex 开始扫描 allocBits,直到遇到一个 0,表示找到一个空闲对象。
// 随后,freeindex 会调整为刚发现的空闲对象之后的位置,以便下次扫描从新的位置开始。
//
// 如果 freeindex == nelem,表示这个 span 中没有空闲对象。
freeindex uint16
//该span中的object的数量
nelems uint16
//是 allocBits 的部分缓存,且保存的是 allocBits 的补码。
allocCache uint64
//mspan的等级
spanclass spanClass // size class and noscan (uint8)
}
[*]next与prev用于指向同规格下的上一mspan与下一mspan,将整条mspan封装成链表,有助于扩展和烧毁。
[*]startAddr用于记载起始地址。
[*]nelems用于记载当前mspan中object的数量。
[*]freeindex用于标识下一次扫描探求object的位置,在该位置前的object都已经被使用。
2.3、object
object是协程应用逻辑一次向Golang申请的对象。object是golang内存管理模型针对内存分配更加细化的内存管理单元,一个mspan在初始化时会被划分为多个object。例如一个大小为8B的object归属于大小为8KB的mspan,该mspan被划分为1024个object。object根据大小可以从8B~32KB划分为67种。golang内存管理内部本身用来给对象存储内存的基本单元是object。
下图可以展示object、page、mspan三者间的关系。
https://img2024.cnblogs.com/blog/3542244/202412/3542244-20241219221224760-458960584.png
2.4、SizeClass与SpanClass
SizeClass是针对Object的大小来举行划分的等级,标识着每次申请空间的容量对应着哪一个等级。例如一次内存哀求中,申请获得1B~7B之间的容量,那都归属于SizeClass 1级别。
而SpanClass是针对mspan来划分的,指span大小的级别。(固然mspan的大小只能为page的整数倍,最高只能为32KB,但是因为一个mspan可以被不同大小的object划分,因此mspan具有多个种类)。一个SizeClass对应着两个SpanClass,其中一个SpanClass为存放必要GC扫描的对象,而另一个则存放不必要GC扫描的对象。
其对应关系图可以用下图来表示。
https://img2024.cnblogs.com/blog/3542244/202412/3542244-20241219221234220-188445880.png
通过sizeclass天生spanclass的源码如下:
type spanClass uint8
// uint8 左 7 位为 mspan 等级,最右一位标识是否为 noscan
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
}天生规则为现将sizeclass左移一位,即乘2,最小位标识是否为noscan。
2.5、mcache
mcache被一个P持有,作为其本地缓存,当运行在和当前P绑定的线程上,必要申请内存资源时,会优先从mcache上获得,因为一个P在同一时刻只能有一个M在其上运行,因此访问mcache不必要持有锁,加速了内存分配。
mcache在初始化时,持有每一种spanclass的一个mspan实体,不同spanclass的Mspan长度会不同
其源码的核心字段如下:
type mcache struct {
//微对象分配器
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
//缓存的mspan,每一种spanclass有一个mspan
alloc *mspan // spans to allocate from, indexed by spanClass
}mcache总的tiny字段用于处理小于16B对象的内存分配,将会在下文提及。
2.6、mcentral
mcentral作为中心缓存,起到分配小对象空间分配的作用,当mcache中的mspan空间不足的时候,就会实验向mcentral获取一份mspan举行补充。有多少个spanclass等级,就存在着多少个mcentral,每一个mcentral只负责自己等级的mspan分配。
核心字段如下:
type mcentral struct {
spanclass spanClass
//维护全部空闲的span集合
partial spanSet
//维护存在非空闲的span集合
full spanSet
}mcentral持有两个mspan集合,一个集合用于存放含有可用空间的mspan即partial集合,另一个则存放没有可用空间的mspan即full集合。每一个集合长度为2,是因为有一条用于处理GC。
2.7、mheap
对于golang的上层应用而言,mheap就是它们眼中的操纵系统假造内存,通过向mheap申请内存而不是每次都向操纵系统申请开辟空间,可以淘汰其开销。mheap的上游就是mcentral,当mcentral的内存不够时,就会以page为单元向mheap哀求空间,而当mheap的空间不够时,则会向下游的操纵系统申请空间,申请的单元为64M。
type mheap struct { // 堆的全局锁 lock mutex // 空闲页分配器 pages pageAlloc // 记载了所有的 mspan. 必要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装天生的 allspans []*mspan // heapAreana 数组,64 位系统下,二维数组容量为 // 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256T arenas [1
页:
[1]