2026-10-31·by Sijie Wang

GC

Go GC:三色标记,并发还要正确

动机:自动回收,又不能长时间停全世界。 Go 程序不停分配、从不手动 free,runtime 必须自己找出垃圾。笨办法是全停下来,走一遍所有可达对象,剩下的全 free,但长停顿就是长跑服务的延迟毛刺。所以 Go 一边让程序跑、一边并发标记,整个设计都是为了让并发标记不出错。

三色:没看、半看、全看。 所有对象起手都是白色,意思是未访问、候选回收。根(全局变量、栈上的指针、寄存器)先染成灰色。灰色是已访问、子对象还没扫完;黑色是已访问、子对象全扫完了。标记就是不断取一个灰对象,把它的子对象染灰,再把它自己变黑,直到没有灰对象为止。最后还白着的,就是不可达的垃圾。白灰黑正好对应没看、半看、全看。

三色不变式,以及并发怎么破坏它。 强不变式是:黑对象不能直接指向白对象。如果标记时这条一直成立,就不会漏标活对象。但标记时程序还在改指针,两种改动凑在一起就破了它:一个黑对象新指了一个白对象,同时这个白对象唯一的另一条路被删了。白对象还活着,但已经没有灰对象能通向它,标记结束都没访问过它,就会被当成垃圾 free 掉。这就是整个设计要防的致命错误。弱不变式也值得知道:黑对象指白对象可以,但这个白对象必须同时经由某个灰对象可达,也就是有灰色保护路径通向它。屏障的活,就是让弱不变式在任何时候都成立。

写屏障:写指针时介入。 标记期间每次写指针,屏障都跑一遍。插入屏障是:黑对象新指白对象时,把白对象染灰。删除屏障是:指针被覆盖或删除时,把旧目标染灰,防那是通向它的最后一条路。Go 用的是两者混合:堆上的写走屏障,栈上的写不走屏障,靠终止停顿重扫栈来兜底。屏障的活就一句话:还活着的白对象,不能在标记结束时还是没看过的。

STW 只发生在两端。 标记开始先短停一次:开写屏障、把根染灰。然后并发标记,程序照跑。标记结束再停一次:重扫根和栈、关写屏障、算下次触发点。两次停顿之外全是并发跑。这两次停顿的长度,就是长跑服务延迟毛刺的来源。

标记完了:清扫。 白对象被回收,内存拿回去复用。清扫本身也可以跟程序并发跑,不用再停一次。

GOGC:调的是触发点,不是算法。 GOGC 定的是上次回收后堆能长多少才触发下次:下次触发点等于上次存活堆乘以一加 GOGC 除以一百。默认 100 就是堆可以翻倍才触发下次。GOGC 大,回收次数少、内存多;GOGC 小,回收勤、内存少。它调的是回收频率和内存的 tradeoff,别的东西都不管。旁边还可以再设一个软上限 GOMEMLIMIT。

标记分摊在 P 上。 标记任务由 P 上的 worker 分担,分配太快、标记跟不上时,分配的 goroutine 会被拉去辅助标记:先干一部分标记活,再继续分配。分配者替标记买单,标记就不会被分配甩在后面。

生产注记。 长跑后端里 GC 相关的延迟毛刺,根子就是那两次停顿和标记跟不上分配。常规手段按顺序是:调 GOGC 和 GOMEMLIMIT、减少分配、别把堆养得太大。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →