GC 对根对象扫描实现的源码分析

工作池gcWork

工作缓存池(work pool)实现了生产者和消费者模型,用以指向灰色对象。一个灰色对象在工作队列中被扫描标记。一个黑色对象表示已被标记不在队列中。

写屏障、根发现、栈扫描和对象扫描都会生成一个指向灰色对象的指针。扫描消费时会指向这个灰色对象,从而将先其变为黑色,再扫描它们,此时可能会产生一个新的指针指向灰色对象。这个就是三色标记法的基本知识点,应该很好理解。

gcWork 是为垃圾回收器提供的一个生产和消费工作接口。

它可以用在stack上,如

(preemption must be disabled)
gcw := &getg().m.p.ptr().gcw
.. call gcw.put() to produce and gcw.tryGet() to consume ..

在标记阶段使用gcWork可以防止垃圾收集器转换到标记终止,这一点很重要,因为gcWork可能在本地持有GC工作缓冲区。可以通过禁用抢占(systemstackacquirem)来实现。

Continue reading

Runtime: Golang GC源码分析

在阅读此文前,需要先了解一下三色标记法以及混合写屏障这些概念。

源文件 src/runtime/mgc.go 版本 1.16.2。

基本知识

在介绍GC之前,我们需要认识有些与GC相关的基本信息,如GC的状态、模式、统计信息等。

三种状态

共有三种状态

const (
	_GCoff             = iota // GC not running; sweeping in background, write barrier disabled
	_GCmark                   // GC marking roots and workbufs: allocate black, write barrier ENABLED
	_GCmarktermination        // GC mark termination: allocate black, P's help GC, write barrier ENABLED
)
  • _GCoff GC未运行
  • _GCmark 标记中,启用写屏障
  • _GCmarktermination 标记终止,启用写屏障

三种模式

支持三种模式:

const (
    gcBackgroundMode gcMode = iota // concurrent GC and sweep
    gcForceMode                    // stop-the-world GC now, concurrent sweep
    gcForceBlockMode               // stop-the-world GC now and STW sweep (forced by user)
)
  • gcBackgroundMode 默认模式,标记与清扫过程都是并发执行的
  • gcForceMode 只在清扫阶段支持并发;
  • gcForceBlockMode GC全程需要STW。

针对每种模式,在标记阶段会采用不同的标记策略,详细见  gcBgMarkWorker() 

Continue reading

Golang中的切片与GC

今天再看 timer 源码的时候,在函数 clearDeletedTimers() 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。

// src/runtime/time.go

// The caller must have locked the timers for pp.
func clearDeletedTimers(pp *p) {
	timers := pp.timers
	......
	// 对无用的切片元素赋值 nil
	for i := to; i < len(timers); i++ {
		timers[i] = nil
	}

	atomic.Xadd(&pp.deletedTimers, -cdel)
	atomic.Xadd(&pp.numTimers, -cdel)
	atomic.Xadd(&pp.adjustTimers, -cearlier)

	timers = timers[:to]
	pp.timers = timers
	updateTimer0When(pp)

	......
}

变量 to 指新切片的长度, len(timers)指原来切片的长度。

这里在其进行 timers = timers[:to] 操作前,先是将 to 数组索引后的值进行了赋值 nil。按照我们平常的用法,是没有必要执行这一步的,那为什么这里要加这一步呢?主要还是与GC 有关。

在日常开发中很少注意到这个细节,虽然最终实现的结果是一样的,但如果考虑GC的话,差别可就很大了。

Continue reading

学习Golang GC 必知的几个知识点

对于gc的介绍主要位于 src/runtime/mgc.go,以下内容是对注释的翻译。

GC 四个阶段

通过源文件注释得知GC共分四个阶段:

  1. GC 清理终止 (GC performs sweep termination
    a. Stop the world, 每个P 进入GC safepoint(安全点),从此刻开始,万物静止
    b. 清理未被清理的span,如果GC被强制执行时才会出现这些未清理的span
  2. GC 标记阶段(GC performs the mark phase
    a. 将gc标记从 _GCoff 修改为 _GCmark,开启写屏障(write barries)和 协助助手(mutator assists),将根对象放入队列。 在STW期间,在所有P都启用写屏障之前不会有什么对象被扫描。
    b. Start the world恢复STW)。标记工作线程和协助助手并发的执行。对于任何指针的写操作和指针值,都会被写屏障覆盖,使新分配的对象标记为黑色。
    c. GC 执行根标记工作。包括扫描所有的栈,全局对象和不在堆数据结构中的堆指针。每扫描一个栈就会导致goroutine停止,把在栈上找到的所有指针置灰色,然后再恢复goroutine运行。
    d. GC 遍历队列中的每个灰色对象,扫描完以后将灰色对象标记为黑色,并将其指向的对象标记为灰色。
    e. 由于GC工作在分布本地缓存中,采用了一种 “分布式终止算法(distributed termination algorithm)” 来检测什么时候没有根对象或灰色对象。在这个时机GC会转为标记中止(mark termination)。
  3. 标记终止(GC performs mark termination
    a. Stop the world从此刻开始,万物静止
    b. 设置阶段为 _GCmarktermination,并禁用 工作线程worker和协助助手
    c. 执行清理,flush cache
  4. 清理阶段(GC performs the sweep phase
    a. 设置清理阶段标记为 _GCoff,设置清理状态禁用写屏障
    b. Start the world恢复STW),从现在开始,新分配的对象是白色的。如有必要,请在请在使用前扫描清理
    c. GC在后台执行并发扫描,并响应分配

整个GC共四个阶段,每次开始时从上到下执行。第一步是清理上次未清理完的span,而不是直接标记阶段。具体的流程可以参考 runtime.GC() 函数

Continue reading

Golang什么时候会触发GC

Golang采用了三色标记法来进行垃圾回收,那么在什么场景下会触发这个GC动作呢?

源码主要位于文件 src/runtime/mgc.go go version 1.16

触发条件从大方面来说,分为 手动触发系统触发 两种方式。手动触发一般很少用,主要通过开发者调用 runtime.GC() 函数来实现,而对于系统自动触发是 运行时 根据一些条件自行维护的,这也正是本文要介绍的内容。

不管哪种触发方式,底层回收机制是一样的,所以我们先看一下手动触发,看看能否根据它来找GC触发所需的条件。

// src/runtime/mgc.go

// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
	n := atomic.Load(&work.cycles)

	// 等待上一轮的标记终止
	gcWaitOnMark(n)

	// We're now in sweep N or later. Trigger GC cycle N+1, which
	// will first finish sweep N if necessary and then enter sweep
	// termination N+1.
	// 触发GC
	gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})

	// Wait for mark termination N+1 to complete.
	// 等待本轮 标记终止
	gcWaitOnMark(n + 1)

	......
}

可以看到开始执行GC的是 gcStart() 函数,它有一个 gcTrigger 参数,是一个触发条件结构体,它的结构体也很简单。

Continue reading