Go垃圾回收系列之八:辅助标记

Go语言精选

共 1539字,需浏览 4分钟

 · 2021-03-18

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() 让渡当前辅助标记的执行权利。而如果是当前逻辑处理器的工作池中没有多余的标记工作可做,则会陷入到休眠状态,直到后台工作协程扫描了足够的任务后后,刷新全局资产池并将等待中的协程唤醒。



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


浏览 9
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报