golang垃圾回收概述

news/2024/7/8 6:46:17

golang垃圾回收

golang的垃圾回收机制已经迭代过好几次了,主要的几个演进过程如下:

  1. v1.0版本中使用标记和清除算法,需要再整个gc过程中暂定程序。
  2. V1.5版本中实现了三色标记清除的并发垃圾回收。
  3. v1.8使用混合写屏障技术提升了垃圾收集的时间。

主要的三个大节点的更改如下所示,但是golang的垃圾回收的演进过程细节越来越复杂,性能也越来越好。首先,先了解一下基本的标记和清楚、三色标记清除的基本流程。

垃圾回收的两种机制标记清除和三色标记清除

标记清除

标记清除的主要流程就是如下:

  1. 暂停程序用户业务程序运行。
  2. 标记出不可达的对象(标记阶段)。
  3. 回收好标记的不可达的对象(清除阶段)。
  4. 恢复用户业务程序运行。

举例如下:

用户程序
对象A
对象B
对象C
对象D
对象E

此时再扫描过程中,就会发现对象A,对象B,对象C和对象D都是被标记为可达的,对象E标记为不可达对象,此时对象E就会在清除阶段被回收掉。原理与实现过程相对简单不复杂。

优点缺点
1.实现简单,易于维护1.需要暂定程序,对用户程序影响大
2.需要扫描整个heap,工作量较大

在golang的早起版本使用的方式就是如下:

启动STW
Mark标记
Sweep清除
停止STW

整个过程都是阻塞用户程序,这样会导致gc的过程时间较长,后续通过三色标记清除来进行优化改进。

三色标记清除

三色标记的原理是将程序中的对象分为白色、黑色和灰色三种;

  • 白色,潜在可能会被回收的对象,如果标记完成之后,白色对象会被回收
  • 黑色对象,正在被使用的对象,黑色对象中任何一个指针都不可能直接到达白色对象,该对象已经扫描完毕
  • 灰色对象,已被回收器访问到的对象,但灰色对象仍有可能指向白色对象,故需要继续扫描

举例如下

用户程序
对象A
对象B
对象C
对象D
对象E
初始化状态
白色对象列表 = []
灰色对象列表 = []
黑色对象列表 = []开始扫描叫从用户程序出发开始扫描
用户程序可达到对象A,对象D,故将这两个对象标记为灰色,此时队列状态如下;
白色对象列表 = [对象B,对象C,对象E]
灰色对象列表 = [对象A,对象D]
黑色对象列表 = []此时从灰色对象(因为从用户程序扫描完成之后就没有从根节点出发的对象)第二次扫描
对象D没有其他节点故标记为黑色
对象A可达对象B,对象A标记为黑色,对象B标记为灰色
白色对象列表 = [对象C,对象E]
灰色对象列表 = [对象B]
黑色对象列表 = [对象A,对象D]再一次扫描灰色对象列表
此时对象B可达对象C,故对象B标记为黑色,对象C标记为灰色
白色对象列表 = [对象E]
灰色对象列表 = [对象C]
黑色对象列表 = [对象A,对象D,对象B]再一次扫描灰色对象列表
此时对象C没有可达,扫描完成标记为黑色对象,此时灰色队列列表为空停止扫描
白色对象列表 = [对象E]
灰色对象列表 = []
黑色对象列表 = [对象A,对象D,对象B,对象C]最后白色列表对象只有对象E,对象E会在标记完成之后被回收掉

如上基本上就是三色标记的基础原理,从执行流程上看,如果不采用其他的措施,也需要进行程序停止,然后进行三色标记,最后进行回收,因为如果不停止程序用户的话,在扫描完成之后,黑色对象直接指向一个白色对象,但是白色对象回被回收掉,从而引起数据丢失,造成程序错误。

在golang中,为了将gc的过程可以和用户程序进行同步运行,每次gc的时候尽量的少让用户程序暂停,故引入了写屏障技术来保证开启了三色标记的时候,也能够让用户程序来继续运行。

写屏障技术

在垃圾回收的过程中,满足强三色不变或者弱三色不变,可保证在回收标记过程中对象不丢失或者出错。

  • 强三色不变,不存在黑色对象引用到白色对象的指针
  • 弱三色不变,所有被黑色对象引用的白色对象都处于灰色状态

有了这两个条件之后,我们就可以进行如下操作。

// 用户使用对象时
添加对象(当前下游对象slot, 新下游对象ptr) {   //1标记灰色(新下游对象ptr)   //2当前下游对象slot = 新下游对象ptr                    
}

所有新插入的对象都标记成灰色重新接受检查。

// 用户程序添加用户
添加对象(当前下游对象slot, 新下游对象ptr) {//1if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {标记灰色(当前下游对象slot)     //slot为被删除对象, 标记为灰色}//2当前下游对象slot = 新下游对象ptr
}

如果添加的对象为灰色,则将当前要删除的对象进行标记重新检查。

当在用户程序中使用了如上的操作之后,就可以将三色标记的过程可以和用户程序并行,并可保证垃圾回收的正确性。此时也可以使用并发的垃圾回收来提升响应,也可以分段进行垃圾回收将每一次stw的时间尽量压短一些。

在golang的实现过程中,主要通过混合写屏障来保证在gc与程序代码并行的时候,标记的正确性,

writePointer(slot, ptr):shade(*slot)if any stack is grey:shade(ptr)*slot = ptr

同时将写入待写入和写入的过程中都进行标记,从而在gc并行标记的过程完成之后不需要重新扫描各个堆栈,从而可以增量进行垃圾回收,并且在整个过程中(v1.8之后),都是栈空间不启动,堆空间启动混合写屏障,整个过程就变的效率较高,不需要停止其他的用户程序,栈空间上面的对象都是标记为黑色,堆上面的操作都开启屏障,从而提升了整个gc的效率。

golang垃圾回收过程

gc启动的三种情况

gc的启动节点主要分三种情况:

  • 后台触发,比如sysmon和forcegchelper,通过定时策略或者监控来启动gc
  • 通过runtime.GC这个函数来手动gc
  • 通过在runtime.mallogc申请内存的时候,来检查是否需要开启gc
后台启动

在后台触发的情况下主要是通过forcegchelper代码来监控原子锁是否被唤醒了

// start forcegc helper goroutine
func init() {go forcegchelper()
}func forcegchelper() {forcegc.g = getg()for {lock(&forcegc.lock)   						// 获取锁if forcegc.idle != 0 {throw("forcegc: phase error")}atomic.Store(&forcegc.idle, 1)    goparkunlock(&forcegc.lock, waitReasonForceGGIdle, traceEvGoBlock, 1)  // 将协程设置到等待状态// this goroutine is explicitly resumed by sysmonif debug.gctrace > 0 {println("GC forced")}// Time-triggered, fully concurrent.  // 开始调用gcgcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})}
}

在sysmon函数中有如下一段来检查是否需要开始gc

		// check if we need to force a GCif t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {lock(&forcegc.lock)forcegc.idle = 0var list gListlist.push(forcegc.g)  			// 设置当前的g为可用运行的状态,等待injectglist(&list)unlock(&forcegc.lock)}

此时就会调用gcStart来进行垃圾回收。

手动调用GC函数

如果显示的调用runtime.GC函数也会开始启动垃圾回收。

func GC() {// We consider a cycle to be: sweep termination, mark, mark// termination, and sweep. This function shouldn't return// until a full cycle has been completed, from beginning to// end. Hence, we always want to finish up the current cycle// and start a new one. That means://// 1. In sweep termination, mark, or mark termination of cycle// N, wait until mark termination N completes and transitions// to sweep N.//// 2. In sweep N, help with sweep N.//// At this point we can begin a full cycle N+1.//// 3. Trigger cycle N+1 by starting sweep termination N+1.//// 4. Wait for mark termination N+1 to complete.//// 5. Help with sweep N+1 until it's done.//// This all has to be written to deal with the fact that the// GC may move ahead on its own. For example, when we block// until mark termination N, we may wake up in cycle N+2.// Wait until the current sweep termination, mark, and mark// termination complete.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.gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})// Wait for mark termination N+1 to complete.gcWaitOnMark(n + 1)// Finish sweep N+1 before returning. We do this both to// complete the cycle and because runtime.GC() is often used// as part of tests and benchmarks to get the system into a// relatively stable and isolated state.for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {sweep.nbgsweep++Gosched()}// Callers may assume that the heap profile reflects the// just-completed cycle when this returns (historically this// happened because this was a STW GC), but right now the// profile still reflects mark termination N, not N+1.//// As soon as all of the sweep frees from cycle N+1 are done,// we can go ahead and publish the heap profile.//// First, wait for sweeping to finish. (We know there are no// more spans on the sweep queue, but we may be concurrently// sweeping spans, so we have to wait.)for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 {Gosched()}// Now we're really done with sweeping, so we can publish the// stable heap profile. Only do this if we haven't already hit// another mark termination.mp := acquirem()cycle := atomic.Load(&work.cycles)if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {mProf_PostSweep()}releasem(mp)
}

此时就调用gcStart来进行垃圾回收。

分配内存调用mallocgc函数

当调用mallocgc函数时,也会检查是否需要开始gc;

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if shouldhelpgc {if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {gcStart(t)   // 开始gc}}return x
}

如上就是三种启动gc的时机。

gc的流程

gc的主要流程如下;

  • 开始清理阶段,先暂停用户运行程序,所有的程序进入到STW状态,如果当前的垃圾回收机制是强制的还需要进行未处理的垃圾处理
  • 开始标记阶段,将状态切换至_GCmark状态、开启写屏障、GC辅助等,然后再恢复用户程序执行,标记协程和用户运行的协程同步运行,通过写屏障来保证所有的标记都是正确的,此时新创建的对象都是黑色,删除和替换的指针都是灰色。
  • 标记终止阶段,此时停止用户程序运行,将状态切换到_GCmarktermination,清理掉所有的缓存,设置gcSweep运行的协程,然后重置标记的状态,保存清理的一些信息,最后恢复用户程序运行,同时也会被调度到垃圾回收程序释放内存。

首先查看一下开始阶段gcStart

// gcStart starts the GC. It transitions from _GCoff to _GCmark (if
// debug.gcstoptheworld == 0) or performs all of GC (if
// debug.gcstoptheworld != 0).
//
// This may return without performing this transition in some cases,
// such as when called on a system stack or with locks held.
func gcStart(trigger gcTrigger) {// Since this is called from malloc and malloc is called in// the guts of a number of libraries that might be holding// locks, don't attempt to start GC in non-preemptible or// potentially unstable situations.mp := acquirem()          // 判断当前g是否可以抢占,如果不能则不执行gcif gp := getg(); gp == mp.g0 || mp.locks > 1 || mp.preemptoff != "" {releasem(mp)return}releasem(mp)mp = nil// Pick up the remaining unswept/not being swept spans concurrently//// This shouldn't happen if we're being invoked in background// mode since proportional sweep should have just finished// sweeping everything, but rounding errors, etc, may leave a// few spans unswept. In forced mode, this is necessary since// GC can be forced at any point in the sweeping cycle.//// We check the transition condition continuously here in case// this G gets delayed in to the next GC cycle.for trigger.test() && sweepone() != ^uintptr(0) {   // 检查是否可以开启垃圾收集sweep.nbgsweep++}// Perform GC initialization and the sweep termination// transition.semacquire(&work.startSema)// Re-check transition condition under transition lock.if !trigger.test() {                  // 在加锁的情况下检查是否可以执行gcsemrelease(&work.startSema)return}// For stats, check if this GC was forced by the user.   设置gc状态work.userForced = trigger.kind == gcTriggerAlways || trigger.kind == gcTriggerCycle// In gcstoptheworld debug mode, upgrade the mode accordingly.// We do this after re-checking the transition condition so// that multiple goroutines that detect the heap trigger don't// start multiple STW GCs.mode := gcBackgroundMode      // 判断当前的gc状态,是否开启并发gcif debug.gcstoptheworld == 1 {mode = gcForceMode} else if debug.gcstoptheworld == 2 {mode = gcForceBlockMode}// Ok, we're doing it! Stop everybody elsesemacquire(&worldsema)if trace.enabled {   // 跟踪处理traceGCStart()}// Check that all Ps have finished deferred mcache flushes.for _, p := range allp {if fg := atomic.Load(&p.mcache.flushGen); fg != mheap_.sweepgen {println("runtime: p", p.id, "flushGen", fg, "!= sweepgen", mheap_.sweepgen)throw("p mcache not flushed")}}gcBgMarkStartWorkers()    // 启动后台扫描任务来运行gcResetMarkState()   			// 重置标记状态work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocsif work.stwprocs > ncpu {// This is used to compute CPU time of the STW phases,// so it can't be more than ncpu, even if GOMAXPROCS is.work.stwprocs = ncpu}work.heap0 = atomic.Load64(&memstats.heap_live)work.pauseNS = 0work.mode = modenow := nanotime()work.tSweepTerm = nowwork.pauseStart = nowif trace.enabled {traceGCSTWStart(1)}systemstack(stopTheWorldWithSema)   		// 停止所有的G// Finish sweep before we start concurrent scan.systemstack(func() {finishsweep_m()   									// 清扫上一轮gc未清扫的垃圾})// clearpools before we start the GC. If we wait they memory will not be// reclaimed until the next GC cycle.clearpools()work.cycles++gcController.startCycle()work.heapGoal = memstats.next_gc// In STW mode, disable scheduling of user Gs. This may also// disable scheduling of this goroutine, so it may block as// soon as we start the world again.   if mode != gcBackgroundMode {    // 判断是否是并发gcschedEnableUser(false)}// Enter concurrent mark phase and enable// write barriers.//// Because the world is stopped, all Ps will// observe that write barriers are enabled by// the time we start the world and begin// scanning.//// Write barriers must be enabled before assists are// enabled because they must be enabled before// any non-leaf heap objects are marked. Since// allocations are blocked until assists can// happen, we want enable assists as early as// possible.setGCPhase(_GCmark)        // 设置状态为标记状态gcBgMarkPrepare() // Must happen before assist enable.   重置后台标记任务的计数gcMarkRootPrepare()                 //计算扫描根对象的数量// Mark all active tinyalloc blocks. Since we're// allocating from these, they need to be black like// other allocations. The alternative is to blacken// the tiny block on every allocation from it, which// would slow down the tiny allocator.gcMarkTinyAllocs()     // 标记所有的tiny alloc等待合并的对象// At this point all Ps have enabled the write// barrier, thus maintaining the no white to// black invariant. Enable mutator assists to// put back-pressure on fast allocating// mutators.atomic.Store(&gcBlackenEnabled, 1)   // 启动辅助gc// Assists and workers can start the moment we start// the world.gcController.markStartTime = now// Concurrent mark.systemstack(func() {now = startTheWorldWithSema(trace.enabled)   // 重新开启用户程序运行work.pauseNS += now - work.pauseStartwork.tMark = now})// In STW mode, we could block the instant systemstack// returns, so don't do anything important here. Make sure we// block rather than returning to user code.if mode != gcBackgroundMode {Gosched()}semrelease(&work.startSema)
}

开启了回收任务,此时开启了后台任务来扫描并开启了辅助回收。接着开始了Mark的任务数

// gcBgMarkStartWorkers prepares background mark worker goroutines.
// These goroutines will not run until the mark phase, but they must
// be started while the work is not stopped and from a regular G
// stack. The caller must hold worldsema.
func gcBgMarkStartWorkers() {// Background marking is performed by per-P G's. Ensure that// each P has a background GC G.for _, p := range allp {         // 有多少个p就开启多少个协程来标记if p.gcBgMarkWorker == 0 {go gcBgMarkWorker(p)  notetsleepg(&work.bgMarkReady, -1)noteclear(&work.bgMarkReady)}}
}

但是此时启动的worker被调度到的机会也是有限制的,启动了gc协程之后,在被调度的方式如下;

// findRunnableGCWorker returns the background mark worker for _p_ if it
// should be run. This must only be called when gcBlackenEnabled != 0.
func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {if gcBlackenEnabled == 0 {throw("gcControllerState.findRunnable: blackening not enabled")}if _p_.gcBgMarkWorker == 0 {// The mark worker associated with this P is blocked// performing a mark transition. We can't run it// because it may be on some other run or wait queue.return nil}if !gcMarkWorkAvailable(_p_) {// No work to be done right now. This can happen at// the end of the mark phase when there are still// assists tapering off. Don't bother running a worker// now because it'll just return immediately.return nil}decIfPositive := func(ptr *int64) bool {if *ptr > 0 {if atomic.Xaddint64(ptr, -1) >= 0 {return true}// We lost a raceatomic.Xaddint64(ptr, +1)}return false}if decIfPositive(&c.dedicatedMarkWorkersNeeded) {// This P is now dedicated to marking until the end of// the concurrent mark phase._p_.gcMarkWorkerMode = gcMarkWorkerDedicatedMode} else if c.fractionalUtilizationGoal == 0 {// No need for fractional workers.return nil} else {// Is this P behind on the fractional utilization// goal?//// This should be kept in sync with pollFractionalWorkerExit.delta := nanotime() - gcController.markStartTimeif delta > 0 && float64(_p_.gcFractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {// Nope. No need to run a fractional worker.return nil}// Run a fractional worker._p_.gcMarkWorkerMode = gcMarkWorkerFractionalMode}// Run the background mark workergp := _p_.gcBgMarkWorker.ptr()casgstatus(gp, _Gwaiting, _Grunnable)if trace.enabled {traceGoUnpark(gp, 0)}return gp
}

其中dedicatedMarkWorkersNeeded就是在startCycle函数中,

	totalUtilizationGoal := float64(gomaxprocs) * gcBackgroundUtilizationc.dedicatedMarkWorkersNeeded = int64(totalUtilizationGoal + 0.5)

即百分之二十五的概率gcBackgroundUtilization就是0.25的系数,计算通过之后就设置当前的gp为可运行状态等待调度执行。

此时所有的标记工作都在gcBgMarkWorker函数中完成。

func gcBgMarkWorker(_p_ *p) {gp := getg()type parkInfo struct {m      muintptr // Release this m on park.attach puintptr // If non-nil, attach to this p on park.}     // 保存当前的g和状态// We pass park to a gopark unlock function, so it can't be on// the stack (see gopark). Prevent deadlock from recursively// starting GC by disabling preemption.gp.m.preemptoff = "GC worker init"park := new(parkInfo)gp.m.preemptoff = ""park.m.set(acquirem())park.attach.set(_p_)// Inform gcBgMarkStartWorkers that this worker is ready.// After this point, the background mark worker is scheduled// cooperatively by gcController.findRunnable. Hence, it must// never be preempted, as this would put it into _Grunnable// and put it on a run queue. Instead, when the preempt flag// is set, this puts itself into _Gwaiting to be woken up by// gcController.findRunnable at the appropriate time.notewakeup(&work.bgMarkReady)for {// Go to sleep until woken by gcController.findRunnable.// We can't releasem yet since even the call to gopark// may be preempted.   gopark(func(g *g, parkp unsafe.Pointer) bool {  // 让当前的g进入休眠状态等待被唤醒调度运行park := (*parkInfo)(parkp)        // The worker G is no longer running, so it's// now safe to allow preemption.releasem(park.m.ptr())// If the worker isn't attached to its P,// attach now. During initialization and after// a phase change, the worker may have been// running on a different P. As soon as we// attach, the owner P may schedule the// worker, so this must be done after the G is// stopped.if park.attach != 0 {p := park.attach.ptr()park.attach.set(nil)// cas the worker because we may be// racing with a new worker starting// on this P.if !p.gcBgMarkWorker.cas(0, guintptr(unsafe.Pointer(g))) {// The P got a new worker.// Exit this worker.return false}}return true}, unsafe.Pointer(park), waitReasonGCWorkerIdle, traceEvGoBlock, 0)// Loop until the P dies and disassociates this// worker (the P may later be reused, in which case// it will get a new worker) or we failed to associate.if _p_.gcBgMarkWorker.ptr() != gp {   // 检查g是否一致 break}// Disable preemption so we can use the gcw. If the// scheduler wants to preempt us, we'll stop draining,// dispose the gcw, and then preempt.park.m.set(acquirem())   禁止g被抢占if gcBlackenEnabled == 0 {throw("gcBgMarkWorker: blackening not enabled")}startTime := nanotime()_p_.gcMarkWorkerStartTime = startTimedecnwait := atomic.Xadd(&work.nwait, -1)if decnwait == work.nproc {println("runtime: work.nwait=", decnwait, "work.nproc=", work.nproc)throw("work.nwait was > work.nproc")}systemstack(func() {// Mark our goroutine preemptible so its stack// can be scanned. This lets two mark workers// scan each other (otherwise, they would// deadlock). We must not modify anything on// the G stack. However, stack shrinking is// disabled for mark workers, so it is safe to// read from the G stack.casgstatus(gp, _Grunning, _Gwaiting)   // 设置当前g未等待运行状态switch _p_.gcMarkWorkerMode {          // 判断后台标记任务的模式default: throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")case gcMarkWorkerDedicatedMode:      // 专心执行标记任务gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)if gp.preempt {// We were preempted. This is// a useful signal to kick// everything out of the run// queue so it can run// somewhere else.lock(&sched.lock)for {gp, _ := runqget(_p_)if gp == nil {break}globrunqput(gp)}unlock(&sched.lock)}// Go back to draining, this time// without preemption.gcDrain(&_p_.gcw, gcDrainFlushBgCredit)case gcMarkWorkerFractionalMode:  // 适当执行直到被抢占gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)case gcMarkWorkerIdleMode:     // 只有在空闲的情况下执行gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)}casgstatus(gp, _Gwaiting, _Grunning)    // 设置当前的g为运行状态})// Account for time.duration := nanotime() - startTimeswitch _p_.gcMarkWorkerMode {case gcMarkWorkerDedicatedMode:atomic.Xaddint64(&gcController.dedicatedMarkTime, duration)atomic.Xaddint64(&gcController.dedicatedMarkWorkersNeeded, 1)case gcMarkWorkerFractionalMode:atomic.Xaddint64(&gcController.fractionalMarkTime, duration)atomic.Xaddint64(&_p_.gcFractionalMarkTime, duration)case gcMarkWorkerIdleMode:atomic.Xaddint64(&gcController.idleMarkTime, duration)}// Was this the last worker and did we run out// of work?incnwait := atomic.Xadd(&work.nwait, +1)if incnwait > work.nproc {println("runtime: p.gcMarkWorkerMode=", _p_.gcMarkWorkerMode,"work.nwait=", incnwait, "work.nproc=", work.nproc)throw("work.nwait > work.nproc")}// If this worker reached a background mark completion// point, signal the main GC goroutine.   判断是否所有的后台标记任务完成if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {// Make this G preemptible and disassociate it// as the worker for this P so// findRunnableGCWorker doesn't try to// schedule it._p_.gcBgMarkWorker.set(nil)releasem(park.m.ptr())gcMarkDone()   // 准备进入完成标记状态// Disable preemption and prepare to reattach// to the P.//// We may be running on a different P at this// point, so we can't reattach until this G is// parked.park.m.set(acquirem())park.attach.set(_p_)}}
}

主要通过不同的标志位来进入gcDrain函数,使任务标记的工作再不同的模式下运行。

当所有的标记的任务都完成之后,就会调用gcMarkDone函数。

// gcMarkDone transitions the GC from mark to mark termination if all
// reachable objects have been marked (that is, there are no grey
// objects and can be no more in the future). Otherwise, it flushes
// all local work to the global queues where it can be discovered by
// other workers.
//
// This should be called when all local mark work has been drained and
// there are no remaining workers. Specifically, when
//
//   work.nwait == work.nproc && !gcMarkWorkAvailable(p)
//
// The calling context must be preemptible.
//
// Flushing local work is important because idle Ps may have local
// work queued. This is the only way to make that work visible and
// drive GC to completion.
//
// It is explicitly okay to have write barriers in this function. If
// it does transition to mark termination, then all reachable objects
// have been marked, so the write barrier cannot shade any more
// objects.
func gcMarkDone() {// Ensure only one thread is running the ragged barrier at a// time.semacquire(&work.markDoneSema)top:// Re-check transition condition under transition lock.//// It's critical that this checks the global work queues are// empty before performing the ragged barrier. Otherwise,// there could be global work that a P could take after the P// has passed the ragged barrier.if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {semrelease(&work.markDoneSema)return}// Flush all local buffers and collect flushedWork flags.gcMarkDoneFlushed = 0systemstack(func() {gp := getg().m.curg// Mark the user stack as preemptible so that it may be scanned.// Otherwise, our attempt to force all P's to a safepoint could// result in a deadlock as we attempt to preempt a worker that's// trying to preempt us (e.g. for a stack scan).casgstatus(gp, _Grunning, _Gwaiting)forEachP(func(_p_ *p) {// Flush the write barrier buffer, since this may add// work to the gcWork.wbBufFlush1(_p_)// For debugging, shrink the write barrier// buffer so it flushes immediately.// wbBuf.reset will keep it at this size as// long as throwOnGCWork is set.if debugCachedWork {b := &_p_.wbBufb.end = uintptr(unsafe.Pointer(&b.buf[wbBufEntryPointers]))b.debugGen = gcWorkPauseGen}// Flush the gcWork, since this may create global work// and set the flushedWork flag.//// TODO(austin): Break up these workbufs to// better distribute work._p_.gcw.dispose()// Collect the flushedWork flag.if _p_.gcw.flushedWork {atomic.Xadd(&gcMarkDoneFlushed, 1)_p_.gcw.flushedWork = false} else if debugCachedWork {// For debugging, freeze the gcWork// until we know whether we've reached// completion or not. If we think// we've reached completion, but// there's a paused gcWork, then// that's a bug._p_.gcw.pauseGen = gcWorkPauseGen// Capture the G's stack.for i := range _p_.gcw.pauseStack {_p_.gcw.pauseStack[i] = 0}callers(1, _p_.gcw.pauseStack[:])}})casgstatus(gp, _Gwaiting, _Grunning)})if gcMarkDoneFlushed != 0 {if debugCachedWork {// Release paused gcWorks.atomic.Xadd(&gcWorkPauseGen, 1)}// More grey objects were discovered since the// previous termination check, so there may be more// work to do. Keep going. It's possible the// transition condition became true again during the// ragged barrier, so re-check it.goto top}if debugCachedWork {throwOnGCWork = true// Release paused gcWorks. If there are any, they// should now observe throwOnGCWork and panic.atomic.Xadd(&gcWorkPauseGen, 1)}// There was no global work, no local work, and no Ps// communicated work since we took markDoneSema. Therefore// there are no grey objects and no more objects can be// shaded. Transition to mark termination.now := nanotime()work.tMarkTerm = nowwork.pauseStart = nowgetg().m.preemptoff = "gcing"if trace.enabled {traceGCSTWStart(0)}systemstack(stopTheWorldWithSema)    // 停止用户程序运行// The gcphase is _GCmark, it will transition to _GCmarktermination// below. The important thing is that the wb remains active until// all marking is complete. This includes writes made by the GC.if debugCachedWork {// For debugging, double check that no work was added after we// went around above and disable write barrier buffering.for _, p := range allp {gcw := &p.gcwif !gcw.empty() {printlock()print("runtime: P ", p.id, " flushedWork ", gcw.flushedWork)if gcw.wbuf1 == nil {print(" wbuf1=<nil>")} else {print(" wbuf1.n=", gcw.wbuf1.nobj)}if gcw.wbuf2 == nil {print(" wbuf2=<nil>")} else {print(" wbuf2.n=", gcw.wbuf2.nobj)}print("\n")if gcw.pauseGen == gcw.putGen {println("runtime: checkPut already failed at this generation")}throw("throwOnGCWork")}}} else {// For unknown reasons (see issue #27993), there is// sometimes work left over when we enter mark// termination. Detect this and resume concurrent// mark. This is obviously unfortunate.//// Switch to the system stack to call wbBufFlush1,// though in this case it doesn't matter because we're// non-preemptible anyway.restart := falsesystemstack(func() {for _, p := range allp {wbBufFlush1(p)if !p.gcw.empty() {restart = truebreak}}})if restart {getg().m.preemptoff = ""systemstack(func() {now := startTheWorldWithSema(true)work.pauseNS += now - work.pauseStart})goto top}}// Disable assists and background workers. We must do// this before waking blocked assists.atomic.Store(&gcBlackenEnabled, 0)// Wake all blocked assists. These will run when we// start the world again.gcWakeAllAssists()    // 唤醒所有的辅助GC// Likewise, release the transition lock. Blocked// workers and assists will run when we start the// world again.semrelease(&work.markDoneSema)// In STW mode, re-enable user goroutines. These will be// queued to run after we start the world.schedEnableUser(true)// endCycle depends on all gcWork cache stats being flushed.// The termination algorithm above ensured that up to// allocations since the ragged barrier.nextTriggerRatio := gcController.endCycle()   // 计算下一次gc的heap大小// Perform mark termination. This will restart the world.gcMarkTermination(nextTriggerRatio)      // 进入完成标记阶段
}

gcMarkTermination进入到完成标记阶段。

func gcMarkTermination(nextTriggerRatio float64) {// World is stopped.// Start marktermination which includes enabling the write barrier.atomic.Store(&gcBlackenEnabled, 0)setGCPhase(_GCmarktermination)work.heap1 = memstats.heap_livestartTime := nanotime()mp := acquirem()mp.preemptoff = "gcing"_g_ := getg()_g_.m.traceback = 2gp := _g_.m.curgcasgstatus(gp, _Grunning, _Gwaiting)gp.waitreason = waitReasonGarbageCollection// Run gc on the g0 stack. We do this so that the g stack// we're currently running on will no longer change. Cuts// the root set down a bit (g0 stacks are not scanned, and// we don't need to scan gc's internal state).  We also// need to switch to g0 so we can shrink the stack.systemstack(func() {gcMark(startTime)// Must return immediately.// The outer function's stack may have moved// during gcMark (it shrinks stacks, including the// outer function's stack), so we must not refer// to any of its variables. Return back to the// non-system stack to pick up the new addresses// before continuing.})systemstack(func() {work.heap2 = work.bytesMarkedif debug.gccheckmark > 0 {// Run a full non-parallel, stop-the-world// mark using checkmark bits, to check that we// didn't forget to mark anything during the// concurrent mark process.gcResetMarkState()initCheckmarks()gcw := &getg().m.p.ptr().gcwgcDrain(gcw, 0)wbBufFlush1(getg().m.p.ptr())gcw.dispose()clearCheckmarks()}// marking is complete so we can turn the write barrier offsetGCPhase(_GCoff)gcSweep(work.mode)  // 唤醒后台清扫任务})... // Bump GC cycle count and wake goroutines waiting on sweep.lock(&work.sweepWaiters.lock)memstats.numgc++injectglist(&work.sweepWaiters.list)unlock(&work.sweepWaiters.lock)// Finish the current heap profiling cycle and start a new// heap profiling cycle. We do this before starting the world// so events don't leak into the wrong cycle.mProf_NextCycle()systemstack(func() { startTheWorldWithSema(true) })   // 重新开启用户程序运行// Flush the heap profile so we can start a new cycle next GC.// This is relatively expensive, so we don't do it with the// world stopped.mProf_Flush()// Prepare workbufs for freeing by the sweeper. We do this// asynchronously because it can take non-trivial time.prepareFreeWorkbufs()// Free stack spans. This must be done between GC cycles.systemstack(freeStackSpans)   // 释放内存...releasem(mp)mp = nil// now that gc is done, kick off finalizer thread if neededif !concurrentSweep {// give the queued finalizers, if any, a chance to runGosched()}
}

该函数大概流程就是保存相关的信息并开启清扫任务并开启用户程序运行。至此最基本的垃圾回收的过程就完成了。

总结

golang的垃圾回收机制细节很多很复杂,主要是通过三色标记清楚和写屏障来保证gc的高效,在启动gc的时候先STW,启动相关的标记协程,然后再恢复运行,此时用户程序与标记程序同时运行,当标记程序标记完成的时候再次STW,此时就清理gc相关的数据与信息,并启动回收的协程然后再次恢复运行,此时垃圾回收与用户程序同时运行,从而完成一次垃圾回收,通过现在这种优化,大幅提高了垃圾回收的响应效率,减少了STW对用户程序的影响。由于本人才疏学浅,如有错误请批评指正


http://lihuaxi.xjx100.cn/news/257596.html

相关文章

一个「菜鸟」转行AI的成长心得!

↑↑↑关注后"星标"Datawhale每日干货 & 每月组队学习&#xff0c;不错过Datawhale干货 作者&#xff1a;太子长琴&#xff0c;算法工程师本文是一个转行四年的「菜鸟」的成长心得&#xff0c;围绕真实工作场景中&#xff0c;AI算法工程师是如何从头到尾完成一个…

AI大牛李沐装机视频来了!你也能练100亿的大模型

视学算法报道 编辑&#xff1a;Aeneas 拉燕 桃子【导读】AI大牛李沐带你来装机&#xff01;AI大牛沐神来装机了&#xff0c;还是训练100亿参数模型那种。在还没出装机视频前&#xff0c;李沐老师曾发起了一个小小的问卷调查&#xff0c;趁着显卡降价&#xff0c;看下童鞋们对装…

类加载器双亲委派模式

双亲委派模型的工作流程是&#xff1a;如果一个类加载器收到了类加载的请求&#xff0c;它首先不会自己去尝试加载这个类&#xff0c;而是把请求委托给父加载器去完成&#xff0c;依次向上&#xff0c;因此&#xff0c;所有的类加载请求最终都应该被传递到顶层的启动类加载器中…

如何每天自动备份 SourceSafe (转)

在Microsoft Visual SourceSafe中提到管理员应该每天或者至少每周备份一次SourceSafe中的内容。这里&#xff0c;我们利用现有的工具实现每天自动备份SourceSafe中的内容。<?XML:NAMESPACE PREFIX O />1. 用到的工具a. ssarc.exe. ssarc.exe是随着SourceSafe提供…

PHP下载/采集远程图片到本地

2019独角兽企业重金招聘Python工程师标准>>> PHP下载/采集远程图片到本地01 /** 02* 下载远程图片到本地 03* 04* param string $url 远程文件地址 05* param string $filename 保存后的文件名&#xff08;为空时则为随机生成的文件名&#xff0c;否则为原文件名&am…

Redis 缓存使用技巧和设计方案

欢迎关注方志朋的博客&#xff0c;回复”666“获面试宝典缓存能够有效地加速应用的读写速度&#xff0c;同时也可以降低后端负载&#xff0c;对日常应用的开发至关重要。下面会介绍缓存使用技巧和设计方案&#xff0c;包含如下内容&#xff1a;缓存的收益和成本分析、缓存更新策…

《C#精彩实例教程》小组阅读02 – Visual Studio简介与安装

本图文详细介绍了什么是Visual Studio&#xff0c;以及Visual Studio的安装过程。 上一次我们介绍了Visual Studio 2015的下载&#xff0c;以及MSDN。 这一次&#xff0c;我们说说什么是Visual Studio以及如何安装。 Visual Studio&#xff08;简称VS&#xff09;是微软公司…

Imagination 推新款GPU IP,首次实现桌面级光线追踪效果

游戏界被炒得最热的概念可能就是光线追踪技术了&#xff0c;不仅仅是PC端的游戏。光线追踪所展示出来的画面效果也确实惊艳&#xff0c;可以让我们感叹到图像技术达到的一个新高度。 但是实际上&#xff0c;光线追踪并不是一个新技术。10年前&#xff0c;光追就是游戏玩家茶余…