1. GC回收机制
1.1 V1.3标记清除法
(1)概述
1.STW暂停
STW(暂停业务逻辑,找出可达和不可达对象)
2.对可达对象做上标记
标记完成之后,对象5和对象6不可达,被GC清除.之后STW结束.
(2).缺点
- STW :让程序暂停,程序出现卡顿.
- 标记需要扫描整个heap.
- 清除数据会产生heap碎片.
1.2 V1.5三色标记法
(1) 概述
1.程序创建起初,全部标记为白色,将所有对象放入白色集合中.
2. 将程序的根节点集合展开,遍历Root Set(非递归形式,只遍历一次).得到灰色节点
3.遍历灰色标记表,将可达的对象从白色标记为灰色,遍历之后的灰色,标记为黑色.
4.循环执行第三步,直到灰色标记标中无任何对象.
5.收集所有白色对象(垃圾)
(2) 缺点
如果三色标记法不被STW保护.
当一个白色对象被黑色对象所引用,且灰色对象与它之间的可达关系的白色对象遭到破坏,就会出现对象丢失的情况.
避免上述情况最简单的方式就是STW.
尽量减少STW对程序性能的损耗
- 强三色不变式: 强制行的不允许黑色对象引用白色对象.
- 弱三色不变式: 黑色可以引用白色对象,白色对象存在其它灰色对象对它的引用,或者可达它的链路上游存在灰色对象.
屏障机制实现强弱三色不变式
- 插入屏障: 在A对象引用B对象的时候,B对象被标记为灰色.(将B挂在A下游,B必须被标记为灰色) 满足强三色不变式(不存在黑色对象引用白色对象的情况了,因为白色会强制变成灰色) 插入屏障不在栈上使用.
添加一个下游对象(当前下游对象slot,新下游对象ptr) {
//1
标记灰色(新下游对象ptr)
//2
当前下游对象slot=新下游对象ptr
}
插入屏障流程:
由于并发特性,此刻外界向对象4添加添加对象8,对象1添加对象9.对象4在堆区,即将触发插入屏障机制.
当灰色标记表中为空,准备回收白色标记之前,重新遍历扫描一次栈空间.此时加STW暂停保护栈,防止外界干扰.(这也是插入写屏障的不足,结束时需要STW重新扫描栈,10~100ms).
- 删除屏障: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色. 满足弱三色不变式(保护灰色对象到白色对象的路径不会断)
添加下游对象(当前下游对象slot , 新下游对象ptr) {
//1
if 当前下游对象slot是灰色 || 白色{
标记灰色(当前下游对象slot)
}
//2
当前下游对象slot = 新下游对象ptr
}
删除屏障流程:
灰色对象1删除对象5,如果不触发删除写屏障,5-2-3路径与主链路断开,最后均会被当作垃圾清除.
删除写屏障回收精度低,一个对象即使被删除了最后一个指向它的指针,也依旧可以活过这一轮,在下一轮的GC中被清理掉.
1.3 V1.8混合写屏障机制
(1) 概述
具体操作:
- GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
- GC期间,任何在栈上创建的新对象,均为黑色.
- 被删除的对象标记为灰色.
- 被添加的对象标记为灰色.
混合写屏障流程:
1.GG刚开始,默认都为白色.
2.三色标记法,优先扫描全部栈对象,将可达对象均标记为黑.
2. GPM调度模型设计
2.1. GPM
- G(goroutine协程):Go协程
- P(Processor处理器):包含运行Go代码的必要资源,也有调度go协程的能力
- M(machine线程):工作线程,由操作系统调度
P的本地队列存放的是等待运行的G,数量不能超过256G,优先将新创建的G放在P的本地队列中,如果满了会放在全局队列中.
GOMAXPROCS:是在程序启动时创建的,指定P的个数.可以通过环境变量,或者在程序中通过runtime.GOMAXPROCS()来设置.(允许相应个数的go协程在同一时刻并行执行)
Go语言本身限制M的最大量为10000.一般M的个数会略大于P.
2.2. 调度器设计策略
- 复用线程:避免频繁的创建,销毁线程.而是对线程复用.
-
- work stealing机制: 当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程.
- hand off机制: 当本线程因为G进行调用阻塞时,线程将释放绑定的P,将P转移给其他空闲线程执行.
- 利用并行: GOMAXPROCS设置P的数量,最多由GOMAXPROCS个线程分布在多个CPU上同时运行.
- 抢占:在Go中,一个goroutine最多占用CPU10ms , 防止其他goroutine长期得不到执行.
- 全局G队列:当M执行work stealing从其他P中偷不到G时,它可以从全局G队列获取G.
2.3. go func()
- 有两个存储G的队列,一个是局部调度器P的本地队列,一个全局队列.新创建的G会先保存在P的本地队列中,如果P的本地队列中已满就会保存在全局的队列中.
- G只能运行在M中,一个M必须持有一个P.M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,偷取其他队列的P来执行 .
- 一个M调度G执行的过程是一个循环机制.
- 当M执行某一个G时候如果发生了syscall或其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务这个P.
- 当M系统调用结束后,这个G会尝试获取一个空闲的P执行;并放入到这个P的本地队列.如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中.
2.4. 调度器的生命周期
M0:启动程序后的编号为0的主线程.在全局变量runtime.m0中,不需要再heap上分配.负责执行初始化操作和启动第一个G.启动第一个G之后,M0就和其他M一样了.
G0:每次启动一个M,第一个创建的gourtine,仅用于负责调度的G,不指向任何可执行的函数.在调度或系统调用时,会使用M切换到G0来调度.
2.5. 补充
自旋线程会不断的寻找G(优先寻找全局队列中的G)
从全局队列到P本地队列的负载均衡:从全局队列取的个数: n = min(len(GQ) / GOMAXPROCS + 1 , len(GQ)/2)
GQ:全局队列的总长度
偷取另一个P本地队列中的G: 将要偷取的P中的本地队列一分为二,偷取后一半的G.
自旋线程的最大限制: 自旋线程+执行线程 <= GOMAXPROCS