go的垃圾回收算法

Python013

go的垃圾回收算法,第1张

从Gov1.12版本开始,Go使用了非分代的、并发的、基于三色标记清除的垃圾回收器。

关于垃圾回收,比较常见的算法有引用计数、标记清除和分代收集,Golang语言使用的垃圾回收算法是标记清除。

Golang语言的标记清除垃圾回收算法,为了防止GC扫描时内存变化引起的混乱。那么就需要 STW,即Stop The World。具体在Golang语言中是指,在GC时先停止所有goroutine。再进行垃圾回收,等待垃圾回收结束后再恢复所有被停止的goroutine。

标记清除方法

启动STW,暂停程序的业务逻辑,找出不可达对象和可达对象。

将所有可达对象做标记,清除未标记的对象。停止STW,程序继续执行。循环往复,直到进程程序生命周期结束。因为STW需要暂停程序,为了减少暂停程序的时间。将清除操作移出 STW执行周期,但是优化效果不明显。

所谓三色标记,实际上只是为了方便叙述而抽象出来的一种说法,三色对应垃圾回收过程中对象的三种状态。白色是对象未被标记,gcmarkBits对应位为0,该对象将会在本次GC中被清理。灰色是对象还在标记队列中等待被标记,黑色是对象已被标记,gcmarkBits对应位为0,该对象将会在本次 GC中被回收。

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

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

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

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

了解 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。

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

4. GC 执行清除阶段

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

      b. Start the world ,从现在开始,新分配的对象是白色的,如有必要,在使用 spans 前 allocating 清除 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 抓取一个新的 span 到 mcache 时, goroutine 会清除 mcache 。

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

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

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

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

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

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

获取 GOGC :

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

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

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

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

Golang的内存分配是由golang runtime完成,其内存分配方案借鉴自tcmalloc。

主要特点就是

本文中的element指一定大小的内存块是内存分配的概念,并为出现在golang runtime源码中

本文讲述x8664架构下的内存分配

Golang 内存分配有下面几个主要结构

Tiny对象是指内存尺寸小于16B的对象,这类对象的分配使用mcache的tiny区域进行分配。当tiny区域空间耗尽时刻,它会从mcache.alloc[tinySpanClass]指向的mspan中找到空闲的区域。当然如果mcache中span空间也耗尽,它会触发从mcentral补充mspan到mcache的流程。

小对象是指对象尺寸在(16B,32KB]之间的对象,这类对象的分配原则是:

1、首先根据对象尺寸将对象归为某个SpanClass上,这个SpanClass上所有的element都是一个统一的尺寸。

2、从mcache.alloc[SpanClass]找到mspan,看看有无空闲的element,如果有分配成功。如果没有继续。

3、从mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合适的mspan,返回给mcache。如果没有找到就进入mcentral.grow()—>mheap.alloc()分配新的mspan给mcentral。

大对象指尺寸超出32KB的对象,此时直接从mheap中分配,不会走mcache和mcentral,直接走mheap.alloc()分配一个SpanClass==0 的mspan表示这部分分配空间。

对于程序分配常用的tiny和小对象的分配,可以通过无锁的mcache提升分配性能。mcache不足时刻会拿mcentral的锁,然后从mcentral中充mspan 给mcache。大对象直接从mheap 中分配。

在x8664环境上,golang管理的有效的程序虚拟地址空间实质上只有48位。在mheap中有一个pages pageAlloc成员用于管理golang堆内存的地址空间。golang从os中申请地址空间给自己管理,地址空间申请下来以后,golang会将地址空间根据实际使用情况标记为free或者alloc。如果地址空间被分配给mspan或大对象后,那么被标记为alloc,反之就是free。

Golang认为地址空间有以下4种状态:

Golang同时定义了下面几个地址空间操作函数:

在mheap结构中,有一个名为pages成员,它用于golang 堆使用虚拟地址空间进行管理。其类型为pageAlloc

pageAlloc 结构表示的golang 堆的所有地址空间。其中最重要的成员有两个:

在golang的gc流程中会将未使用的对象标记为未使用,但是这些对象所使用的地址空间并未交还给os。地址空间的申请和释放都是以golang的page为单位(实际以chunk为单位)进行的。sweep的最终结果只是将某个地址空间标记可被分配,并未真正释放地址空间给os,真正释放是后文的scavenge过程。

在gc mark结束以后会使用sweep()去尝试free一个span;在mheap.alloc 申请mspan时刻,也使用sweep去清扫一下。

清扫mspan主要涉及到下面函数

如上节所述,sweep只是将page标记为可分配,但是并未把地址空间释放;真正的地址空间释放是scavenge过程。

真正的scavenge是由pageAlloc.scavenge()—>sysUnused()将扫描到待释放的chunk所表示的地址空间释放掉(使用sysUnused()将地址空间还给os)

golang的scavenge过程有两种: