Go GC 简介

简单了解

GC 与 mutator 线程并发运行,允许多个 GC 线程并行运行

在 GC 的过程中同时运行的 G 称为mutatormutator assist机制就是 G 辅助 GC 做一部分工作的机制。


辅助 GC 做的工作有两种类型,一种是标记(Mark),另一种是清除(Sweep)。

GC 是一个使用写屏障的并发标记和清除。

GC 是非分代的,非紧凑的。

Allocation 是按照大小隔离每个P分配的区域来完成的,以在消除常见情况下的锁的同时,最小化碎片。

GC算法的高级描述

了解 GC 的好地方,可以从 Richard Jones 的gchandbook.org开始。

1. GC 执行清除终止

      a. Stop the world,这将导致所有 P 达到 GC 安全点。

      b. 清除任何未清除过的spans,只有在预期时间之前强制执行此GC周期时,才会有未清除的span

2. GC 执行标记阶段

      a.  准备标记阶段,将gcphase设置为_GCmark(从_GCoff开始),启用写屏障,启用mutator assist,并对根标记作业进行排队。

在所有P都启用写屏障之前,不会扫描任何对象,这是使用STW完成的。

       b. Start the world,从现在开始,GC 工作由调度器启动的标记worker和作allocation的一部分执行的assists来完成。

写屏障将覆写的指针和任何指针写的新指针值都着色。

新分配的对象立即被标记为黑色。

      c.  GC 执行根标记作业。包括:扫描所有栈着色所有全局变量,以及着色堆外运行时数据结构中的任何堆指针

扫描栈会停止goroutine,对goroutine栈中找到的任何指针进行着色,然后恢复goroutine。

       d.  GC 耗尽灰色对象的工作队列,将每个灰色对象扫描为黑色,并对在该对象中找到的所有指针进行着色(反过来可能会将这些指针添加到工作队列中)。

      e.  由于 GC work 分散在本地缓存中,因此 GC 使用分布式终止算法来检测何时不再有根标记作业或灰色对象(参见gcMarkDone函数)。
此时,GC 状态转换到标记终止(gcMarkTermination)。

3. GC 执行标记终止gcMarkTermination

      a. Stop the world

      b. gcphase设置为_GCmarktermination,并禁用 workers 和 assists。

func gcMarkTermination(nextTriggerRatio float64) {
    // World is stopped.
    // Start marktermination which includes enabling the write barrier.
    atomic.Store(&gcBlackenEnabled, 0)
    setGCPhase(_GCmarktermination)
       ......
}

//go:nosplit
func setGCPhase(x uint32) {
    atomic.Store(&gcphase, x)
    writeBarrier.needed = gcphase == _GCmark || gcphase == _GCmarktermination

    //启动写屏障
    writeBarrier.enabled = writeBarrier.needed || writeBarrier.cgo
}

      c. 进行内务整理,如flushing mcaches

确保所有的mcaches都被flushed,每个P将在分配之前刷新自己的mcache,但是空闲的P可能不会。


因为这对于清除所有的span是必要的,所以我们需要确保在下一个 GC 周期开始之前flush完所有mcaches


mcache:是小对象的每个线程缓存(在Go中是per-P)。


不需要锁,因为它是每个线程(per-P)。


mcaches是从非 gc 内存中分配的,因此必须对任何堆指针进行特殊处理。

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...
    link        puintptr
    schedtick   uint32     // incremented on every scheduler call
    syscalltick uint32     // incremented on every system call
    sysmontick  sysmontick // last tick observed by sysmon
    m           muintptr   // back-link to associated m (nil if idle)
    mcache      *mcache   //一个结构体
}

4. GC 执行清除阶段

       a. 准备清除阶段,将gcphase设置为_GCoff,设置清除状态并禁用写屏障。

      b. Start the world,从现在开始,新分配的对象是白色的,如有必要,在使用spansallocating清除spans

       c. GC 在后台进行并发清除并响应allocation,见下面的描述。

5. 当分配足够时,重复上面 1 开始的步骤,参见下面关于GC rate的讨论。

并发清除

清除阶段与正常程序执行并发进行。

在后台goroutine中,堆被惰性(当 goroutine 需要另一个span时)且并发地逐个span扫描(这有助于不是CPU bound的程序)。

STW 标记终止的结尾,所有的span都被标记为需要清除
后台清除器 goroutine 简单地逐个清除span

为了避免在存在未清除的span时请求更多的OS内存,当goroutine需要另一个span时,它首先尝试通过清除来回收这些内存。

当 goroutine 需要分配一个新的小对象span时,它会清除相同大小的小对象span,直到释放至少一个对象为止。

当 goroutine 需要从堆中分配大对象span时,它会清除span,直到将至少那么多页面释放到堆中。

有一种情况,这可能是不够的:如果 goroutine 清除并释放两个不相邻的单页span到堆中,那么它将分配一个新的双页span,但是仍然可以有其他单页未清除的span,可以组合成双页的span

确保在未清除的span上不进行任何操作(这会破坏 GC 位图中的标记位)至关重要。
在 GC 期间,所有mcache都被刷新到中央缓存中,因此它们是空的。

当一个 goroutine 抓取一个新的spanmcache时,goroutine会清除mcache

当 goroutine 显式释放对象或设置finalizer时,goroutine 确保span已经清除(通过清除或者等待并发清除完成)。

finalizer goroutine仅在所有span已经清除时才开始。

当下一次 GC 启动时,它将清除所有尚未清除的span(如果有的话)。

GC rate

下一次 GC 是在我们分配了与已经使用的内存成正比的额外内存量之后。

该比例由GOGC环境变量控制(默认为100)。

如果GOGC=100,而我们使用的是4M,那么当达到8M时,我们将再次进行 GC(此标记在next_gc变量中被跟踪)。

next_gc 在 startCycle函数中被重新计算,即每次gc周期都通过GOGC重新计算下一次gc内存阈值。

获取GOGC

func readgogc() int32 {
    p := gogetenv("GOGC")
    if p == "off" {
        return -1
    }
    if n, ok := atoi32(p); ok {
        return n
    }
    return 100
}

这使得GC成本allocation 成本成线性比例。

调整GOGC只会改变线性常量(以及使用的额外内存量)。

Oblets

为了防止在扫描大型对象时出现长时间的暂停,并提高并行性,垃圾收集器将大于maxObletBytes的对象的扫描作业分解为最多maxObletBytesoblets

当扫描遇到大对象时,它只扫描第一个oblet,并将其余oblets作为新的扫描作业排队。

术语oblets来源:

obletsTcl语言的一个非常简单的对象系统,对象数据存储在全局数组中。

由于Tcl对象系统的普及,oblet主要用于教育目的,供那些希望了解简单的基于数组的对象的基本知识的人使用。                 

TCL经常被用于 快速原型开发,脚本编程,GUI 和 测试 等方面。TCL念作踢叩tickle
使用最广泛的TCL扩展TK,TK提供了各种 OS 平台下的图形用户界面GUI。连强大的Python语言都不单独提供自己的 GUI,而是提供接口适配到TK上。

你可能感兴趣的:(Go GC 简介)