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進行回收。