这是我参与「第五届青训营」伴学笔记创作活动的第7天
笨人纯小白,笔记包括一些上课学到的知识和课外总结的内容,如有错误请指正!
十、Golang内存管理(二:堆内存)
在64位机器上,Go程序启动时,首先会向操作系统申请一块大小为64M的虚拟内存单元,也叫heapArena;
最多可以有2的22次方个内存单元heapArena,所有的heapArena组成了Go的堆内存。
假设物理内存为64G,操作系统给每个进程分配了256T的虚拟内存。
10.1 分级分配
heapArena是Go程序每次申请虚拟内存的单位,这些申请的虚拟内存单位就是Go的堆内存,堆内存由mheap管理。
为了避免出现内存碎片的情况,Go采取了分级分配的策略,根据对象的大小进行内存分配。mheap将这些申请的内存单元heapArena根据对象的大小切分成多个不同规格的小内存块,这些不同规格的内存块称为mspan。
10.2 mspan
mspan被划分为67种,代表着67种内置不同大小的内存块的mspan,每种mspan为N个相同大小的内存块class,可以满足各种对象的内存分配,而且每种mspan的大小也可能不相同。其中四种mspan示意图如下:
- class:代表每种mspan编号。
- bytes/obj:每种mspan中内存块的大小,可以理解为上图中每个格子的大小。
- byte/span:每种mspan的大小,占用堆的
字节数。 - objects:每种mspan中包含多少个小内存块,可以理解为上图中每个mspan的
格子数量。 完整的class表可以在runtime包下的sizeclasses.go中查看。
mspan数据结构:
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // 起始地址
npages uintptr // span中包含的页数
nelems uintptr // 上图span中的class数量,代表可分配的内存块数量
allocCount uint16 // 已分配的内存块(格子)个数
spanclass spanClass // span中内存块的规格
elemsize uintptr // span中每一个内存块大小
allocBits *gcBits // 分配位图,代表mspan中每一个内存块的分配情况
gcmarkBits *gcBits // mspan中每一个内存块的标记情况
......
}
10.3 mcentral
为了管理这么多的mspan,于是有了mcentral;每个mcentral用于管理一种特定规格的mspan。
mcentral一共有136种。按理说mcentral是管理mspan的,mspan有67种,为什么mcentral却有136种呢?
其实mspan又分为需要gc扫描和不需要gc扫描的,mspan还有一个class0,用于大对象分配。
mcentral数据结构:
type mcentral struct {
spanclass spanClass
partial [2]spanSet // list of spans with a free object,空闲块span链表
full [2]spanSet // list of spans with no free objects,没有空闲块的span链表
}
mcentral数据结构中并没有锁,协程申请内存时需要向mcentral申请mpsan,此时会调用cacheSpan() *mspan,返回值是*mspan,申请到的mspan会加入到另一个结构中供协程使用,而调用cacheSpan() *mspan的过程中是需要加锁的,在高并发的场景下,多个协程并发的申请锁,锁的频繁加锁解锁的开销非常大,因此就需要刚才说的另一个结构来缓冲这种开销压力。
10.4 mcache
mcache相当于一个协程的本地队列,里面存储着从mcentral申请的mspan,协程向mcentral申请mspan需要加锁,为了避免多个协程申请内存不断加锁,于是引入了mcache,这个思想就是参考了GMP模型的本地协程队列。
mcentral是全局资源,为多个协程服务,当协程内存不足时会向mcentral申请。
mcache数据结构:
type mcache struct {
...
alloc [numSpanClasses]*mspan // 保存着申请到的mpsan, numSpanClasses = 68 << 1
...
}
mcache在初始化时是没有任何mspan的,在使用过程中会动态的从mcentral中申请并保存至alloc 中,这样协程需要申请内存时,就直接从自己的本地缓存中获取mspan,避免了加锁。
mcache中相同类型的mspan通过
链表连接。