天天看點

Go : GC

1、何時觸發GC?

當堆上配置設定大于32k的對象的時候開始檢測是否滿足GC條件,滿足則開始自動GC。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
shouldhelpgc := false
// 配置設定的對象小于 32K byte
if size <= maxSmallSize {
    ...
} else {
    shouldhelpgc = true
    ...
}
...
// gcShouldStart() 函數進行觸發條件檢測
if shouldhelpgc && gcShouldStart(false) {
    // gcStart() 函數進行垃圾回收
    gcStart(gcBackgroundMode, false)
}
}
           

另外,還可以通過調用runtime.GC()進行主動垃圾回收,主動垃圾回收的過程是主動式的。

// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire
// program.
func GC() {
	gcStart(gcForceBlockMode, false)
}
           

此外,Golang本身也會對運作狀态監控,如果超過2分鐘沒有Gc,則觸發GC。監控函數會在主goroutine中啟動

2、GC的觸發條件

初始化的時候會設定一個gc的觸發門檻值,當堆上的活躍對象大于門檻值的時候則會觸發GC

3、Go的垃圾回收的算法使用的是三色标記法

三色标記法的主要流程:

1、開始時所有對象都為白色。

2、從root開始找可達對象,将其标記為灰色,放入待處理隊列。

3、周遊灰色對象隊列,灰色對象的引用對象标記為灰色放入待處理隊列,同時将自身标為黑色。

4、周遊完灰色隊列對象後,清掃白色标記的對象。

三色标記法類似于标記清掃算法。但三色标記的好處在于可以讓使用者程式和Mark并發執行。

在go中,灰色對象隊列使用gcw來管理灰色對象。gcw的結構體是gcWork,gcWork的核心在wbuf1和wbuf2,這裡存儲的就是灰色對象。

至于為什麼采用2個buf來做緩沖呢,官方是這麼解釋的:

This can be thought of as a stack of both work buffers' 	pointers concatenated. When we pop the last pointer, we shift the stack up by one work buffer by bringing in a new full buffer and discarding an empty one. When we fill both buffers, we shift the stack down by one work buffer by bringing in a new empty buffer and discarding a full one. This way we have one buffer's worth of hysteresis, which amortizes the cost of getting or putting a work buffer over at least one buffer of work and reduces contention on the 	global work lists.
           

大緻意思就是可以認為這2個buf是一個串聯在一起的堆棧。在使用的時候就可以分攤開銷程本,并且可以減少在global work lists上的競争。

type p struct {
...
gcw gcWork
}

type gcWork struct {
	// wbuf1 and wbuf2 are the primary and secondary work buffers.
wbuf1, wbuf2 wbufptr

// 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
}	
           

4、GC過程

1、STW phase 1

這個階段主要是在GC開始前做準備工作。

func gcStart(mode gcMode, forceTrigger bool) {
	 //在背景啟動 mark worker 
    if mode == gcBackgroundMode {
        gcBgMarkStartWorkers()
}
	...
    if mode == gcBackgroundMode {
    // GC 開始前的準備工作

    //處理設定 GCPhase,setGCPhase 會啟動 write barrier
    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()
  	
    // Start The World
    systemstack(startTheWorldWithSema)
} else {
    ...
}
}
           

2、Mark

這個階段是并行的。從程式運作後一直在背景待着,大部分時候是在休眠狀态,在gcstart()被調用後gcBgMarkWorker會被啟動。

// mark 過程
    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)
    switch _p_.gcMarkWorkerMode {
    default:
        throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
    case gcMarkWorkerDedicatedMode:
        gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
    case gcMarkWorkerFractionalMode:
        gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
    case gcMarkWorkerIdleMode:
        gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
    }
    casgstatus(gp, _Gwaiting, _Grunning)
    })
           

從這部分可以看出,當MarkWorker被啟動後,會調用gcDrain()函數,由gcDrain實作Mark.

// gcDrain scans roots and objects in work buffers, blackening grey
	// objects until all roots and work buffers have been drained.
	func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	 ...	
  // Drain root marking jobs.
   if work.markrootNext < work.markrootJobs {
    for !(preemptible && gp.preempt) {
        job := atomic.Xadd(&work.markrootNext, +1) - 1
        if job >= work.markrootJobs {
            break
        }
        markroot(gcw, job)
        if idle && po。llWork() {
            goto done
        }
    }
    }

    // 處理 heap 标記
    // Drain heap marking jobs.
    for !(preemptible && gp.preempt) {
    ...
    //從灰色列隊中取出對象
    var b uintptr
    if blocking {
        b = gcw.get()
    } else {
        b = gcw.tryGetFast()
        if b == 0 {
            b = gcw.tryGet()
        }
    }
    if b == 0 {
        // work barrier reached or tryGet failed.
        break
    }
    //掃描灰色對象的引用對象,标記為灰色,入灰色隊列
    scanobject(b, gcw)
    }
	}
           

3、Mark termination(Stw phase2)

這個階段在go的1.8版本後以及不會對goroutine stack進行re-scan了。

4、清掃

清掃有2種方式:阻塞式和并行式

阻塞就是正常跑, 并行式是用鎖的方式實作的。

func gcSweep(mode gcMode) {
	  ...
 //阻塞式
   if !_ConcurrentSweep || mode == gcForceBlockMode {
    // Special case synchronous sweep.
    ...
    // Sweep all spans eagerly.
    for sweepone() != ^uintptr(0) {
        sweep.npausesweep++
    }
    // Do an additional mProf_GC, because all 'free' events are now real as well.
    mProf_GC()
    mProf_GC()
    return
}

    // 并行式
    // Background sweep.
    lock(&sweep.lock)
    if sweep.parked {
    sweep.parked = false
    ready(sweep.g, 0, true)
    }
    unlock(&sweep.lock)
}
           

go的記憶體管理都是基于span的,在标記時針對對象的span标記,清掃的時候掃描span,對沒标記的span進行回收。