Go垃圾回收系列之八:辅助标记
Go1.5引入了并发标记之后,带来了许多新的问题。例如,在并发标记阶段、由于在扫描内存的同时,用户协程也在不断的分配内存。因此当用户协程的内存分配足够快,快到后台标记协程来不及扫描,那么GC标记阶段将永远不会结束,从而无法完成完整的GC周期,造成内存泄露。
为了解决这样的问题,引用了辅助标记策略。辅助标记必须是在垃圾回收的标记阶段,用户协程由于分配了超过限度的内存、而不得不暂停用户协程并切换到辅助标记工作。所以一个简单的策略是
需要扫描的内存 = M
在并发标记期间、一旦新分配了内存M,就必须完成M 的扫描工作。但是我们前面看到过,对于像obj这样的对象,并不需要扫描对象中所有的内存。
type obj struct{
*int a
*T b
intc
float d
}
因此扫描策略可以调整为:
需要扫描的内存 = assistWorkPerByte* M
其中 assistWorkPerByte < 1 ,代表每个字节需要完成多少扫描工作。并且真实需要扫描的内存会少于实际的内存数量。
在GC并发标记阶段,工作协程分配内存时,会首先检查是否现在已经完成了指定数量的扫描工作。当前协程中的gcAssistBytes字段代表当前协程可以分配的内存数量,类似于资产池。当本地的gcAssistBytes不足时,会尝试从全局的资产池中偷取。工作协程一开始是没有资产的,所有的资产都来自于后台标记协程。
// gcBlackenEnabled在GC的标记阶段会开启
if gcBlackenEnabled != 0 {
assistG = getg()
assistG.gcAssistBytes -= int64(size)
// 需要辅助标记
if assistG.gcAssistBytes < 0 {
// 会按分配的大小判断需要协助GC完成多少工作
gcAssistAlloc(assistG)
}
}
从图中可以看出,用户协程中的本地资产来自于后台标记协程的扫描工作。之前提到,需要扫描的内存X = assistWorkPerByte* M 。反过来,当后台标记协程已经扫描了X内存,意味着可以容忍分配的内存数量为M = X/assistWorkPerByte。这种机制保证了GC并发标记时,工作协程分配的内存数量不至于过多,也不会限制得太少。
当工作协程在分配内存时,无法从本地资产池也无法从全局资产池获取到资产,这时需要停止工作协程,并执行辅助标记协程。辅助标记协程只会辅助标记协程自己需要扫描的工作量assistWorkPerByte* M ,当扫描完成指定工作完成或者被抢占时会退出。当辅助标记完成后,本地仍然没有足够的资产。这可能是因为当前被抢占,也可能是当前逻辑处理器的工作池中没有多余的标记工作。被抢占时,会调用Gosched() 让渡当前辅助标记的执行权利。而如果是当前逻辑处理器的工作池中没有多余的标记工作可做,则会陷入到休眠状态,直到后台工作协程扫描了足够的任务后后,刷新全局资产池并将等待中的协程唤醒。
推荐阅读