Go内存模型与分配机制
在操作系统中,存在
寄存器、高速缓存、内存和磁盘,越接近cpu存储的容量越小,其对应的价格就越高昂。页表、分页管理等机制来减少内存碎片。
Golang中的内存模型,以空间换时间,一次缓存,多次复用。堆mheap正是基于该思想,产生的数据结构。依次细化粒度,建立了 mcentral、mcache 的模型,下面对三者作个梳理:
mheap:全局的内存起源,访问要加全局锁;mcentral:每种对象大小规格(全局共划分为68种)对应的缓存,锁的粒度也仅限于同一种规格以内;mcache:每个P(正是GMP中的P)持有一份的内存缓存,访问时无锁,多级规格,提高利用率;
内存单元mspan
page和mspan的2个概念,page:最小的存储单元,默认大小为8KB,mspan大小为page的整数倍,且从8B到80KB 被划分为67种不同的规格,对应源代码在runtime/sizeclasses.go,mspan具有如下特点:
- 根据规格大小,产生了等级的制度,
mspan是Golang内存管理的最小单元,runtime/mheap.go; - 消除了外部碎片,但不可避免会有内部碎片;
- 宏观上能提高整体空间利用率,同等级的
mspan会从属同一个mcentral,最终会被组织成链表,因此带有前后指针(prev、next); - 正是因为有了规格等级的概念,才支持
mcentral实现细锁化,全局总览,留个印象; mspan会基于bitMap辅助快速找到空闲内存块(块大小为对应等级下的object大小),此时需要使用到Ctz64算法.
线程缓存mcache
mcache是每个P独有的缓存,因此交互无锁;mcache将每种spanClass等级的mspan各缓存了一个,总数为2(nocan维度) *68(大小维度)=136;mcache中还有一个为对象分配器tiny allocator,用于处理小于16B对象的内存分配;
中心缓存mcentral
要点:
- 每个
mcentral对应一种spanClass; - 每个
mcentral下聚合了该spanClass下的mspan; mcentral下的mspan分为两个链表,分别为有空间mspan链表partial和满空间mspan链表full`;- 每个
mcentral一把锁;
全局堆缓存mheap
- 对于
Golang上层应用而言,堆是操作系统虚拟内存的抽象,以页(8KB)为单位,作为最小内存存储单元; - 负责将连续页组装成
mspan,全局内存基于bitMap标识其使用情况,每个bit对应一页,为0则自由,为1则已被mspan组装; - 通过
heapArena聚合页,记录了页到mspan的映射信息,建立空闲页基数树索引radix tree index,辅助快速寻找空闲页; - 是
mcentral的持有者,持有所有spanClass下的mcentral,作为自身的缓存,内存不够时,向操作系统申请,申请单位为 heapArena(64M);
对象分配流程
不论是以下哪种方式,最终都会殊途同归步入mallocgc方法中,例如:new(T)、&T{}、make(xxxx),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)步.
Go垃圾回收原理
做
Java的都对GC比较熟悉,在JVM中常见的GC算法有:标记整理(Mark-Sweep)、标记压缩(Mark-Compact)、半空间复制(类似于G1),通过引用计数寻找不可达对象,便于垃圾回收。回收算法
Go V1.3之前是标记清除(mark and sweep)算法,步骤如下:
- 第一步,先暂停程序业务逻辑,找出不可达对象和可达对象。
- 第二步,开始标记,程序找出它所有可达的对象,并做上标记。
- 第三步,标记完了之后,然后开始清除未标记的对象。
- 第四步,停止暂停,让程序继续跑,然后循环重复这个过程,直到
process程序声明周期结束。
标记-清除(mark and sweep)方法的缺点有:
1) stop the world, 让程序暂停,程序出现卡顿(重要问题);2)标记需要扫描整个heap;3)清除数据会产生heap碎片;
Go V1.5引入的三色标记法,对象分为三种颜色标记:黑、灰、白,黑对象代表,对象自身存活,且其指向对象都已标记完成;灰对象代表,对象自身存活,但其指向对象还未标记完成;白对象代表,对象尙未被标记到,可能是垃圾对象。- 第一步,就是只要是新创建的对象,默认的颜色标记都是”白色”;
- 第二步,每次
GC回收开始,然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入”灰色”集合; - 第三步,遍历灰色集合,将灰色对象应用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合;
- 第四步,重复第三步,直到灰色中无任何对象;
三色标记法的不足之处,如果三色标记法不被STW保护,下面2个条件满足,就会出现对象丢失的现象:
- 条件
1,一个白色对象被黑色对象引用(白色对象被挂在黑色下); - 条件
2,灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了改白色);
强弱三色不变式,破坏条件1和条件2,阻止引用对象丢失。
- 屏障机制,不足之处有,在结束时需要
STW来重新扫描栈,大约需要10~100ms。插入屏障,对象被引用时触发的机制(栈空间对象不触发):- 具体操作,在
A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色,无论B当前时什么颜色)。满色,强三色不变式(不存在黑色对象引用白色对象的情况了,因为白色对象会强制变成灰色)。 - 场景1:
A添加下游对象(nil, B)//A之前没有下游,新添加一个下游对象B,B被标记为灰色。 - 场景2:
A添加下游对象(C, B)//A将下游C更换为B,B被标记为灰色。
- 具体操作,在
为避免栈上的对象被遗漏,在堆标记完成。准备回收白色对象前,会重新扫描一次栈(栈上所有对象都被标记为白色),另外,会加STW暂停保护栈,防止外界干扰。
删除屏障,对象被删除时触发的机制,具体操作:被删除的对象,如果自身为灰色或白色,那么被标记为灰色。
- 不足之处有,回收精度低,一个对象即使被删除了最后一个指向它的指针,它也依旧可以活过这一轮,在下一轮
GC中被清理掉。
- 混合写屏障操作
Go V1.8引入的三色标记法 + 混合写屏障机制,具体操作如下,变形的弱三色不变式(结合了插入、删除写屏障两者的特点):- 1)
GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW); - 2)
GC期间,任何栈上创建的新对象,均为黑色; - 3)被删除的对象标记为灰色,被添加的对象标记为灰色;
- 1)
场景1,对象被一个堆对象删除引用,成为栈对象的下游。在混合模式下,栈上的对象也不需要STW了。
场景2,对象被一个栈对象删除,成为另一个栈对象的下游。
场景3,对象被一个堆对象删除引用,成为另一个堆对象的下游。
场景4,对象从一个栈对象删除引用,成为另一个堆对象的下游。