Go垃圾回收[8]-辅助标记

363次阅读  |  发布于2年以前

80%血复活了,微信公众号没有留言功能,实在是坑啊.....

前情回顾

[* go垃圾回收[7]—] 标记终止与调步算法

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

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8