GC 标记阶段

[TOC]

gcWork 处理器标记对象的工作池

runtime.gcWork 是垃圾收集器中工作池的抽象,它实现了一个生产者和消费者的模型。 每个 P 都有一个本地的 gcWork 。

runtime.gcWork实现了指向灰色对象的指针的生产者/消费者模型。灰色对象是在工作队列上被标记的对象。一个黑色对象被标记,但不在工作队列上。

写屏障、根对象扫描和栈扫描都会向工作池 runtime.gcWork 中增加额外的灰色对象等待处理,而对象的扫描过程会将灰色对象标记成黑色,同时也可能发现新的灰色对象,当工作队列中不包含灰色对象时,整个扫描过程就会结束。

当向 gcWork 中增加或者删除对象时,它总会先操作主缓冲区 wbuf1,一旦主缓冲区空间不足或者没有对象,就会切换到主辅助缓冲区 wbuf2 ;而当两个缓冲区空间都不足时,会从全局的工作缓冲区中插入或者获取对象。

type p struct {
    // gcw是这个P的GC工作缓冲区缓存。工作缓冲区由写屏障填充,由mutator协助清理,并在某些GC状态转换时处理。
	gcw gcWork
    ...
}

type gcWork struct {
	// wbuf1和wbuf2是主工作缓冲区和辅助工作缓冲区.
	// 当向 gcWork 中增加或者删除对象时,它总会先操作主缓冲区 wbuf1,一旦主缓冲区空间不足或者没有对象,就会切换到主辅助缓冲区 wbuf2 ;
	wbuf1, wbuf2 *workbuf

	// Bytes marked (blackened) on this gcWork. This is aggregated
	// into work.bytesMarked by dispose.
	bytesMarked uint64

	// Scan work performed on this gcWork. This is aggregated into
	// gcController by dispose and may also be flushed by callers.
	scanWork int64

	// flushedWork indicates that a non-empty work buffer was
	// flushed to the global work list since the last gcMarkDone
	// termination check. Specifically, this indicates that this
	// gcWork may have communicated work to another gcWork.
	flushedWork bool

	// pauseGen causes put operations to spin while pauseGen ==
	// gcWorkPauseGen if debugCachedWork is true.
	pauseGen uint32

	// putGen is the pauseGen of the last putGen.
	putGen uint32

	// pauseStack is the stack at which this P was paused if
	// debugCachedWork is true.
	pauseStack [16]uintptr
}

runtime.gcWork 提供了为垃圾收集器生产和消费工作的接口。比如这样使用:

// 在栈上执行
gcw := &getg().m.p.ptr().gcw
gcw.put() 	// 用于 produce
gcw.tryGet() // 用于 consume

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

gcWork 与 work

runtime.gcWorkruntime.work 之间的关系。

put()

添加对象到工作缓冲区,主要在 runtime.scanobjectruntime.greyobject 方法中被调用。

tryGet()

从缓冲区获取对象。主要在 runtime.gcDrain 和 runtime.gcDrainN 中被调用。

balance() 平衡处理器负载

为了减少锁竞争,每个P 上都有一个 gcWork 对象,运行时在每个处理器上会保存独立的待扫描工作,然而这会遇到与调度器一样的问题 — 不同处理器的资源不平均,导致部分处理器无事可做,调度器引入了工作窃取来解决这个问题,垃圾收集器也使用了差不多的机制平衡不同处理器上的待处理任务。

balance() 将处理器本地一部分工作放回全局队列中,让其他的处理器处理,保证不同处理器负载的平衡。

// balance moves some work that's cached in this gcWork back on the
// global queue.
//go:nowritebarrierrec
func (w *gcWork) balance() {
	if w.wbuf1 == nil {
		return
	}
	if wbuf := w.wbuf2; wbuf.nobj != 0 {
		w.checkPut(0, wbuf.obj[:wbuf.nobj])
		putfull(wbuf)
		w.flushedWork = true
		w.wbuf2 = getempty()
	} else if wbuf := w.wbuf1; wbuf.nobj > 4 {
		w.checkPut(0, wbuf.obj[:wbuf.nobj])
		w.wbuf1 = handoff(wbuf)
		w.flushedWork = true // handoff did putfull
	} else {
		return
	}
	// We flushed a buffer to the full list, so wake a worker.
	if gcphase == _GCmark {
		gcController.enlistWorker()
	}
}

gcDrain() 颜色标记核心方法

gcDrain() 是用于扫描和标记堆内存中对象的核心方法, 在工作缓冲区 runtime.gcWork 中扫描对象,尝试把灰色对象尽可能多地标记为黑色对象。gcDrain() 根据传入 gcDrainFlags 的不同选择不同的策略。

它可能在GC完成之前返回;调用者的职责是平衡来自其他Ps的工作。

// runtime.gcWork 是垃圾收集器中工作池,它实现了一个生产者和消费者的模型
//go:nowritebarrier
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	if !writeBarrier.needed {
		throw("gcDrain phase incorrect")
	}

	gp := getg().m.curg
    
    // 根据不同的 gcDrainFlags 选择不同的策略
	preemptible := flags&gcDrainUntilPreempt != 0
	flushBgCredit := flags&gcDrainFlushBgCredit != 0
	idle := flags&gcDrainIdle != 0

	initScanWork := gcw.scanWork

	// checkWork is the scan work before performing the next
	// self-preempt check.
	checkWork := int64(1<<63 - 1)
	var check func() bool
	if flags&(gcDrainIdle|gcDrainFractional) != 0 {
		checkWork = initScanWork + drainCheckThreshold
		if idle {
			check = pollWork
		} else if flags&gcDrainFractional != 0 {
			check = pollFractionalWorkerExit
		}
	}

	// 扫描根对象,最先执行的扫描工作
    // 会扫描缓存、数据段、存放全局变量和静态变量的 BSS 段以及 Goroutine 的栈内存
	if work.markrootNext < work.markrootJobs {
        // 如果 可以被抢占,则 暂停,比如其他地方想 STW
		for !(gp.preempt && (preemptible || atomic.Load(&sched.gcwaiting) != 0)) {
			job := atomic.Xadd(&work.markrootNext, +1) - 1
			if job >= work.markrootJobs {
				break
			}
			markroot(gcw, job) //扫描全局变量中的根对象了
            // 使用 check 函数检查否应该退出标记任务并让出该处理器
			if check != nil && check() {
				goto done // 跳到 done 记录 当前已扫描的
			}
		}
	}

    // 扫描堆内存
	// Stop if we're preemptible or if someone wants to STW.
	for !(gp.preempt && (preemptible || atomic.Load(&sched.gcwaiting) != 0)) {
		if work.full == 0 {
			gcw.balance() // 将处理器本地一部分工作放回全局队列中,让其他的处理器处理,保证不同处理器负载的平衡。
		}

		b := gcw.tryGetFast()
		if b == 0 {
			b = gcw.tryGet()
			if b == 0 {
				// Flush the write barrier
				// buffer; this may create
				// more work.
				wbBufFlush(nil, 0)
				b = gcw.tryGet()
			}
		}
		if b == 0 {
			// Unable to get work.
			break
		}
		scanobject(b, gcw) // 从位置 b 开始扫描对象,找到可达活跃对象,标为灰色,待处理

		// Flush background scan work credit to the global
		// account if we've accumulated enough locally so
		// mutator assists can draw on it.
		if gcw.scanWork >= gcCreditSlack {
			atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)
			if flushBgCredit {
				gcFlushBgCredit(gcw.scanWork - initScanWork)
				initScanWork = 0
			}
			checkWork -= gcw.scanWork
			gcw.scanWork = 0

			if checkWork <= 0 {
				checkWork += drainCheckThreshold
				if check != nil && check() {
					break
				}
			}
		}
	}

done:
	// Flush remaining scan work credit.
	if gcw.scanWork > 0 {
		atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)
		if flushBgCredit {
            // 本轮扫描因为外部而中断时,通过 gcFlushBgCredit 记录这次扫描的内存字节数,用于减少辅助标记的工作量。
			gcFlushBgCredit(gcw.scanWork - initScanWork)
		}
		gcw.scanWork = 0
	}
}


gcDrainUntilPreempt   Goroutine  preempt 字段被设置成 true 时返回
gcDrainIdle  调用 runtime.pollWork 函数当处理器上包含其他待执行 Goroutine 时返回
gcDrainFractional  调用 runtime.pollFractionalWorkerExit 函数 CPU 的占用率超过 fractionalUtilizationGoal  20% 时返回
gcDrainFlushBgCredit  调用 runtime.gcFlushBgCredit 计算后台完成的标记任务量以减少并发标记期间的辅助垃圾收集的用户程序的工作量

writeBarrier 写屏障

// The compiler knows about this variable.
var writeBarrier struct {
   enabled bool    // compiler emits a check of this before calling write barrier
   pad     [3]byte // compiler uses 32-bit load for "enabled" field
   needed  bool    // whether we need a write barrier for current GC phase
   cgo     bool    // whether we need a write barrier for a cgo check
   alignme uint64  // guarantee alignment so that compiler can use a 32 or 64-bit load
}

然而写屏障的实现需要编译器和运行时的共同协作。在 SSA 中间代码生成阶段,编译器会使用 cmd/compile/internal/ssa/writebarrier.go 中的 writebarrier 函数在 Store、Move 和 Zero 操作中加入写屏障,生成如下所示的代码:

if writeBarrier.enabled {
  gcWriteBarrier(ptr, val)
} else {
  *ptr = val
}

当 Go 语言进入垃圾收集阶段时,全局变量 runtime.writeBarrier 中的 enabled 字段会被置成开启,所有的写操作都会调用 runtime.gcWriteBarrier 该函数由汇编实现。原本只需一条指令的操作,通过写屏障需要几十条汇编指令完成,所以对性能影响比较大。

TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$120
....
flush:
	...
	CALL	runtime·wbBufFlush(SB)

混合写屏障在开启后,所有新创建的对象都需要被直接标为黑色,这里的标记过程是由 runtime.gcmarknewobject 完成的:

func setGCPhase(x uint32) {
	atomic.Store(&gcphase, x)
	writeBarrier.needed = gcphase == _GCmark || gcphase == _GCmarktermination
	writeBarrier.enabled = writeBarrier.needed || writeBarrier.cgo
}

gcmarknewobject() gc 标黑新对象

runtime.gcmarknewobject 函数把一个新分配对象标记为黑色,

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if gcphase != _GCoff { // 在分配内存时,如果处于 GC 阶段,则标记为黑色
		gcmarknewobject(uintptr(x), size, scanSize)
	}
	...
}

// obj must not contain any non-nil pointers.
//go:nowritebarrier
//go:nosplit
func gcmarknewobject(span *mspan, obj, size, scanSize uintptr) {
	if useCheckmark { // The world should be stopped so this should not happen.
		throw("gcmarknewobject called while doing checkmark")
	}

	// Mark object.
	objIndex := span.objIndex(obj)
	span.markBitsForIndex(objIndex).setMarked() // 通过 markBits 标记为黑色

	// Mark span.
	arena, pageIdx, pageMask := pageIndexOf(span.base())
	if arena.pageMarks[pageIdx]&pageMask == 0 {
		atomic.Or8(&arena.pageMarks[pageIdx], pageMask)
	}

	gcw := &getg().m.p.ptr().gcw
	gcw.bytesMarked += uint64(size)
	gcw.scanWork += int64(scanSize)
}

gcAssistAlloc() 标记辅助

为了保证用户程序分配内存的速度不会超出后台任务的标记速度,运行时还引入了标记辅助技术。 runtime.gcAssistAlloc() 用于辅助标记,

分配多少内存就需要完成多少标记任务


func gcAssistAlloc(gp *g) {
    
}


申请内存时调用的 runtime.gcAssistAlloc 类似 “借债” 和扫描内存时调用的 runtime.gcFlushBgCredit 类似“还债”,通过“借债”和“还债”,来实平衡内存申请和回收尽量处于平衡。

runtime.g 有一个 gcAssistBytes 字段,即每个 Goroutine 持有的 gcAssistBytes 表示当前协程辅助标记的字节数,全局垃圾收集控制器持有的 bgScanCredit 表示后台协程辅助标记的字节数,当本地 Goroutine 分配了较多的对象时,可以使用公用的信用 bgScanCredit 偿还。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	var assistG *g
	if gcBlackenEnabled != 0 {
		assistG = getg()
		if assistG.m.curg != nil {
			assistG = assistG.m.curg
		}
		assistG.gcAssistBytes -= int64(size)

		if assistG.gcAssistBytes < 0 {
			gcAssistAlloc(assistG) // 
		}
	}
	...
	return x
}

gcFlushBgCredit()

markroot() 标记根对象

扫描根对象,禁止抢占

会扫描缓存、数据段、存放全局变量和静态变量的 BSS 段以及 Goroutine 的栈内存;一旦完成了对根对象的扫描,当前 Goroutine 会开始从本地和全局的工作缓存池中获取待执行的任务。

gcMarkDone() 标记终止

runtime.gcMarkDone 如果标记了所有可到达的对象,即没有灰色对象,那么GC将从标记阶段转换到标记终止阶段 _GCmarktermination。如果本地队列中仍然存在待处理的任务,当前方法会将所有的任务加入全局队列并等待其他 Goroutine 完成处理。

这个通常在处理器的本地标记任务完成后,调用。

gcSweep()

在标记完成后,需要关闭混合写屏障、唤醒所有协助垃圾收集的用户程序、恢复用户 Goroutine 的调度,通过调用 runtime.gcMarkTermination 进入标记终止阶段。

调用 runtime.gcSweep 重置清理阶段的相关状态并在需要时阻塞清理所有的内存管理单元;

_GCmarktermination 状态在垃圾收集中并不会持续太久,它会迅速转换至 _GCoff 并恢复应用程序,到这里垃圾收集的全过程基本上就结束了,用户程序在申请内存时才会惰性回收内存

// The world must be stopped.
//go:systemstack
func gcSweep(mode gcMode) {
	if gcphase != _GCoff {
		throw("gcSweep being done but phase is not GCoff")
	}

	lock(&mheap_.lock)
	mheap_.sweepgen += 2
	mheap_.sweepdone = 0
	if !go115NewMCentralImpl && mheap_.sweepSpans[mheap_.sweepgen/2%2].index != 0 {
		// We should have drained this list during the last
		// sweep phase. We certainly need to start this phase
		// with an empty swept list.
		throw("non-empty swept list")
	}
	mheap_.pagesSwept = 0
	mheap_.sweepArenas = mheap_.allArenas
	mheap_.reclaimIndex = 0
	mheap_.reclaimCredit = 0
	unlock(&mheap_.lock)

	...
}

sweepone()

runtime.sweepone 会在堆内存中查找待清理的内存管理单元,查找内存管理单元时会通过 statesweepgen 两个字段判断当前单元是否需要处理。如果内存单元的 sweepgen 等于 mheap.sweepgen - 2,那么就意味着当前单元需要被清理,如果等于 mheap.sweepgen - 1,那么当前管理单元就正在被清理。

gcSweep(), GC(), gcStart(),finishsweep_m()
垃圾收集的清理中包含对象回收器(Reclaimer)和内存单元回收器,这两种回收器使用不同的算法清理堆内存:


对象回收器在内存管理单元中查找并释放未被标记的对象,但是如果 runtime.mspan 中的所有对象都没有被标记,整个单元就会被直接回收,该过程会被 runtime.mcentral.cacheSpan 或者 runtime.sweepone 异步触发;
内存单元回收器会在内存中查找所有的对象都未被标记的 runtime.mspan,该过程会被 runtime.mheap.reclaim 触发;
// sweepone sweeps some unswept heap span and returns the number of pages returned
// to the heap, or ^uintptr(0) if there was nothing to sweep.
func sweepone() uintptr {
    ...
    // Find a span to sweep.
	var s *mspan
	sg := mheap_.sweepgen
	for {
		if go115NewMCentralImpl {
			s = mheap_.nextSpanForSweep()
		} else {
			s = mheap_.sweepSpans[1-sg/2%2].pop()
		}
		if s == nil {
			atomic.Store(&mheap_.sweepdone, 1)
			break
		}
		if state := s.state.get(); state != mSpanInUse {
			// This can happen if direct sweeping already
			// swept this span, but in that case the sweep
			// generation should always be up-to-date.
			if !(s.sweepgen == sg || s.sweepgen == sg+3) {
				print("runtime: bad span s.state=", state, " s.sweepgen=", s.sweepgen, " sweepgen=", sg, "\n")
				throw("non in-use span in unswept list")
			}
			continue
		}
        // 如果内存单元 mspan.weepgen 等于 mheap.sweepgen - 2,那么当前单元需要被清理
		if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
			break
		}
	}

	// Sweep the span we found.
	npages := ^uintptr(0)
	if s != nil {
		npages = s.npages
        if s.sweep(false) {  // 所有的内存回收工作,会通过 mspan.sweep() 进行回收。
			// Whole span was freed. Count it toward the
			// page reclaimer credit since these pages can
			// now be used for span allocation.
			atomic.Xadduintptr(&mheap_.reclaimCredit, npages)
		} else {
			// Span is still in-use, so this returned no
			// pages to the heap and the span needs to
			// move to the swept in-use list.
			npages = 0
		}
	}
    ... 
}

sweepgen

//go:notinheap
type mheap struct {
    ...
    sweepgen  uint32    // sweep generation, see comment in mspan; written during STW
	sweepdone uint32    // all spans are swept
	sweepers  uint32    // number of active sweepone calls
}

// gcBits is an alloc/mark bitmap. This is always used as *gcBits.
//
//go:notinheap
type gcBits uint8

//go:notinheap
type mspan struct {
    ...
    allocBits  *gcBits
	gcmarkBits *gcBits
    // sweep generation:
	// if sweepgen == h->sweepgen - 2, the span needs sweeping
	// if sweepgen == h->sweepgen - 1, the span is currently being swept
	// if sweepgen == h->sweepgen, the span is swept and ready to use
	// if sweepgen == h->sweepgen + 1, the span was cached before sweep began and is still cached, and needs sweeping
	// if sweepgen == h->sweepgen + 3, the span was swept and then cached and is still cached
	// h->sweepgen is incremented by 2 after every GC
	sweepgen    uint32
}

在heap里闲置的span不会被垃圾回收器关注,但 central里的span却有可能正在被清理。所以当 cache从 central提取span时,该属性值就非常重要。

mspan 中维护了一个个内存块,

对象标记状态的存储方式