Go内存管理
注:本文基于Go源码版本1.16
4.1 基本概念
- 虚拟内存
通过内存管理单元MMU把虚拟地址转换成物理地址,加载代码,运行指令,然后释放虚拟内存。
- 栈
从高地址开始,向低地址扩展,内存管理简单,系统自动回收
- 堆
从低地址开始,向高地址扩展,申请和释放内存需要自己动手。我们所说的go的内存管理,实际是对 堆内存的管理,主要通过runtime来分配和管理
4.2 arena、bitmap、spans
4.2.0
Go在程序启动时,先向操作系统申请一块内存(虚拟地址空间),切成小块后自己管理。
申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB.
4.2.1 arena
arena就是堆,把内存切割成8KB大小的page
4.2.2 bitmap
当从堆中申请内存时,需要知道哪些地址保存了对象。bitmap区域用于标识arena区域哪些地址保存了对象。
用4bit标志位标识对象是否包含指针、4bit标志位标识对象是否被GC标记。
bitmap中一个byte大小的内存对应arena区域中4个指针的大小(指针大小为8B)。所以bitmap区域大小为512GB/(4*8B)=16GB
bitmap的高地址部分指向arena区域的低地址部分。bitmap的地址是由高地址向低地址增长的。
4.2.3 spans
spans存放mspan(page组成的内存管理基本单元)的指针,每个指针对应一个page。spans区域的大小为512GB/8KB*8B=512MB
。8KB为一个page的大小。8B为一个指针的大小。
创建mspan时,按页填充对应的spans区域,在回收object时,根据地址很容易找到它所属的mspan
4.3 mspan 内存管理单元
针对申请内存的对象的大小不同,把不同数量的page组合在一起,称为mspan。
mspan是一个包含起始地址、mspan规格,page数量等内容的双向链表。
mspan结构体部分字段如下
type mspan struct {
//链表前向指针,用于将span链接起来
next *mspan
//链表前向指针,用于将span链接起来
prev *mspan
//双向链表
list *mSpanList
//mspan内存的起始位置
startAddr uintptr
//mspan又几个page组成
npages uintptr
// 一共有多少个object
nelems uintptr
//空闲object链表的开始位置
freeindex
//决定object的大小,以及当前mspan是否需要垃圾回收扫描
spanclass spanClass
}
相同npages数的mspan可用组成一个链表。
mspan会被拆解为更小粒度的单位object,sizeclass决定了object的大小。
sizeclass是一个映射列表,实际是一个数组类型[68]uint16
,它的值决定了object的大小。mspan由几个page组成也是sizeclass的值决定的。
go根据class的size,划分了68种mspan。
// 文件位置:`src/runtime/sizeclasses.g`
// 索引0位置被保留使用,具体使用位置后续会讲。
如上文所述,`object`之间采用freelist数据结构构成链表,指针为8Byte所以最小的object大小为8Byte
字段解释:
class: sizeclass值
bytes/obj: 该`mspan`拆分object大小
bytes/span: 该`mspan`是由几pages组成
objects: 该`mspan`共计包含的object数量
tail waste: 该`mspan`拆分为object之后,mspan剩余末尾浪费的内存
// class bytes/obj bytes/span objects tail waste max waste
//
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 24 8192 341 8 29.24%
// 4 32 8192 256 0 21.88%
// 5 48 8192 170 32 31.52%
// 6 64 8192 128 0 23.44%
// 略...
// 62 20480 40960 2 0 6.87%
// 63 21760 65536 3 256 6.25%
// 64 24576 24576 1 0 11.45%
// 65 27264 81920 3 128 10.00%
// 66 28672 57344 2 0 4.91%
// 67 32768 32768 1 0 12.50%
上图列出了67种,还有一种大于32KB的大对象(class=0)从mheap直接分配。
sizeclass的各列解释
1:class的索引,递增
2:object的大小
3:span的大小(span为该种page组合 页数*8KB)
4:可以存储的对象数
5:span%obj的结果
6:最大浪费的内存百分比
- spanclass
Go的内存管理单元mspan被分为两类
第一类:需要垃圾回收扫描的mspan,简称scan
第二类:不需要垃圾回收扫描的mspan,简称nscan
并不是所有mspan会被垃圾回收扫描,为了区分这两类mspan,Go把类型标识和sizeclass的值放在了同一个字段中,如下:
sizeclass的值左移一位,sizeclass << 1
sizeclass的值最后一位存mspan的类型
最后一位为1:不需要垃圾回收扫描的mspan
最后一位为0:需要垃圾回收扫描的mspan
- mspan拆分object实例
以spanclass的10进制值为7的mspan为例
7 >> 1 相当于 00000111(7) -> 0000011(3) 7转为2进制后右移一位 得到的结果十进制的值为3
根据sizeclass可得 mspan的page数为1
object大小为24Byte
可得mspan 共计包含 8192/24 = 341.3333... 约等于341个object
mspan尾部浪费8192-24*341=8Byte
4.4 内存管理组件
4.4.1 mcache线程缓存
每个M会绑定一个P,M从P的本地队列中获取一个G,M运行G。某个时间点上M只会运行一个G。
每个P都有个mcache(本地分配器),当申请一个小于32KB的对象时,会优先从本地的mcahche中查找可用的mspan。
由于每个P都有一个mcache且某个时刻M只会运行一个G,当运行G需要mspan时会去对应P的mcache中寻找,不需要加锁,效率高。
4.4.2 mcentral中央缓存
实际中央缓存central是一个由136个mcentral类型元素的数组构成。
当mcache中没有找到可用的mspan时,会去向mcentral申请。mcentral会为所有的mcache提供切割好的mspan。mcentral根据sizeclass分为68*2种(scan和nscan),每种mcentral管理一种mspan。
lock:因为mcentral是全局的,被所有线程M共享,所以从mcentral获取mspan时要加锁
spanclass:每种mcentral负责的mspan类型
noempty:双端链表,表示还有空闲的mspan可以使用
empty:双端链表,表示这条链表里的mspan都被分配了object,或者是已经被cache取走了的mspan。
- mcache向mcentral申请mspan的流程
1.根据需要分配内存的对象的大小,找到对应sizeclass,sizeclass经过计算得到spanclass,根据spanclass找到对应的mcentral
2.获取加锁,从noempty的链表种找到一个可用的mspan,并将其从noempty中删除,将取出的mspan加入到empty链表中,mspan返回给工作线程,解锁。
3.归还加锁,将mspan从empty链表删除,将mspan加入到noempty链表中,解锁。
4.4.3 mheap堆缓存
当mcentral也没有可用的mspan时,会向mheap申请,mheap管理的就是一开始申请的堆内存。如果mheap没有足够的内存,会向操作系统申请内存。对于大于32k的对象不会经过mcache、mcentral分配器,而会直接从mheap上申请对应数量的page给应用程序。
4.5 具体分配流程
Go的内存分配器在分配对象时,根据对象的大小分为3类:小对象(obj<=16B)、一半对象(16B<obj<=32B)、大对象(>32B)
大体上的分配流程:
- 32KB 的对象,直接从mheap上分配;
- <=16B 的对象使用mcache的tiny分配器分配;
- (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
- 如果mcache没有相应规格大小的mspan,则向mcentral申请
- 如果mcentral没有相应规格大小的mspan,则向mheap申请
- 如果mheap中也没有合适大小的mspan,则向操作系统申请
4.6 tiny分配器
当对象很小时,可能出现以下问题
按照4.5中规则,一个int32类型的变量占4B,应该获取sizeclass=1对应的mspan,也就是每个object是8B.当该变量用掉4B后,还剩4B.对于bool类型的变量占1B,则一个object剩余7B.会造成很多的内碎片。
针对小对象,tiny分配器对于<=16B的内存申请时,会获取sizeclass=2对应的mspan,每个object是16B。多个对象可用复用这个16B的空间,争取把内碎片降到最低(需考虑内存对齐)。
4.7 总结
Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。
Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。