談論Go 什麼時候會觸發 GC問題

在早期經常遭到唾棄的就是在垃圾回收(下稱:GC)機制中 STW(Stop-The-World)的時間過長。那麼這個時候,我們又會好奇一點,作為 STW 的起始,Go 語言中什麼時候才會觸發 GC 呢?

1、什麼是 GC

在計算機科學中,垃圾回收(GC)是一種自動管理內存的機制,垃圾回收器會去嘗試回收程序不再使用的對象及其占用的內存。

最早 John McCarthy 在 1959 年左右發明瞭垃圾回收,以簡化 Lisp 中的手動內存管理的機制(來自 @wikipedia)。

2、為什麼要 GC

手動管理內存挺麻煩,管錯或者管漏內存也很糟糕,將會直接導致程序不穩定(持續泄露)甚至直接崩潰。

3、GC 觸發場景

GC 觸發的場景主要分為兩大類,分別是:

  • 系統觸發:運行時自行根據內置的條件,檢查、發現到,則進行 GC 處理,維護整個應用程序的可用性。
  • 手動觸發:開發者在業務代碼中自行調用 runtime.GC 方法來觸發 GC 行為。

3.1系統觸發

在系統觸發的場景中,Go 源碼的 src/runtime/mgc.go 文件,明確標識瞭 GC 系統觸發的三種場景,分別如下:

const ( 
 gcTriggerHeap gcTriggerKind = iota 
 gcTriggerTime 
 gcTriggerCycle 
) 


  • gcTriggerHeap:當所分配的堆大小達到閾值(由控制器計算的觸發堆的大小)時,將會觸發。
  • gcTriggerTime:當距離上一個 GC 周期的時間超過一定時間時,將會觸發。-時間周期以 runtime.forcegcperiod 變量為準,默認 2 分鐘。
  • gcTriggerCycle:如果沒有開啟 GC,則啟動 GC。

在手動觸發的 runtime.GC 方法中涉及。

3.2手動觸發

在手動觸發的場景下,Go 語言中僅有 runtime.GC 方法可以觸發,也就沒什麼額外的分類的。

但我們要思考的是,一般我們在什麼業務場景中,要涉及到手動幹涉 GC,強制觸發他呢?

需要手動強制觸發的場景極其少見,可能會是在某些業務方法執行完後,因其占用瞭過多的內存,需要人為釋放。又或是 debug 程序所需。

3.3 基本流程

在瞭解到 Go 語言會觸發 GC 的場景後,我們進一步看看觸發 GC 的流程代碼是怎麼樣的,我們可以借助手動觸發的 runtime.GC 方法來作為突破口。

核心代碼如下:

func GC() { 
 n := atomic.Load(&work.cycles) 
 gcWaitOnMark(n) 
 
 gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) 
   
 gcWaitOnMark(n + 1) 
 
 for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) { 
  sweep.nbgsweep++ 
  Gosched() 
 } 
   
 for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 { 
  Gosched() 
 } 
   
 mp := acquirem() 
 cycle := atomic.Load(&work.cycles) 
 if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) { 
  mProf_PostSweep() 
 } 
 releasem(mp) 
} 

在開始新的一輪 GC 周期前,需要調用 gcWaitOnMark 方法上一輪 GC 的標記結束(含掃描終止、標記、或標記終止等)。

開始新的一輪 GC 周期,調用 gcStart 方法觸發 GC 行為,開始掃描標記階段。

需要調用 gcWaitOnMark 方法等待,直到當前 GC 周期的掃描、標記、標記終止完成。

需要調用 sweepone 方法,掃描未掃除的堆跨度,並持續掃除,保證清理完成。在等待掃除完畢前的阻塞時間,會調用 Gosched 讓出。

在本輪 GC 已經基本完成後,會調用 mProf_PostSweep 方法。以此記錄最後一次標記終止時的堆配置文件快照。

結束,釋放 M。

3.4 在哪觸發

看完 GC 的基本流程後,我們有瞭一個基本的瞭解。但可能又有小夥伴有疑惑瞭?

本文的標題是 “GC 什麼時候會觸發 GC”,雖然我們前面知道瞭觸發的時機。但是….Go 是哪裡實現的觸發的機制,似乎在流程中完全沒有看到?

4、監控線程

實質上在 Go 運行時(runtime)初始化時,會啟動一個 goroutine,用於處理 GC 機制的相關事項。

代碼如下:

func init() { 
 go forcegchelper() 
} 
 
func forcegchelper() { 
 forcegc.g = getg() 
 lockInit(&forcegc.lock, lockRankForcegc) 
 for { 
  lock(&forcegc.lock) 
  if forcegc.idle != 0 { 
   throw("forcegc: phase error") 
  } 
  atomic.Store(&forcegc.idle, 1) 
  goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1) 
    // this goroutine is explicitly resumed by sysmon 
  if debug.gctrace > 0 { 
   println("GC forced") 
  } 
 
  gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) 
 } 
} 

在這段程序中,需要特別關註的是在 forcegchelper 方法中,會調用 goparkunlock 方法讓該 goroutine 陷入休眠等待狀態,以減少不必要的資源開銷。

在休眠後,會由 sysmon 這一個系統監控線程來進行監控、喚醒等行為:

func sysmon() { 
 ... 
 for { 
  ... 
  // check if we need to force a GC 
  if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 { 
   lock(&forcegc.lock) 
   forcegc.idle = 0 
   var list gList 
   list.push(forcegc.g) 
   injectglist(&list) 
   unlock(&forcegc.lock) 
  } 
  if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now { 
   lasttrace = now 
   schedtrace(debug.scheddetail > 0) 
  } 
  unlock(&sched.sysmonlock) 
 } 
} 

這段代碼核心的行為就是不斷地在 for 循環中,對 gcTriggerTime now 變量進行比較,判斷是否達到一定的時間(默認為 2 分鐘)。

若達到意味著滿足條件,會將 forcegc.g 放到全局隊列中接受新的一輪調度,再進行對上面 forcegchelper 的喚醒。

5、堆內存申請

在瞭解定時觸發的機制後,另外一個場景就是分配的堆空間的時候,那麼我們要看的地方就非常明確瞭。

那就是運行時申請堆內存的 mallocgc 方法。核心代碼如下:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 
 shouldhelpgc := false 
 ... 
 if size <= maxSmallSize { 
  if noscan && size < maxTinySize { 
   ... 
   // Allocate a new maxTinySize block. 
   span = c.alloc[tinySpanClass] 
   v := nextFreeFast(span) 
   if v == 0 { 
    v, span, shouldhelpgc = c.nextFree(tinySpanClass) 
   } 
   ... 
   spc := makeSpanClass(sizeclass, noscan) 
   span = c.alloc[spc] 
   v := nextFreeFast(span) 
   if v == 0 { 
    v, span, shouldhelpgc = c.nextFree(spc) 
   } 
   ... 
  } 
 } else { 
  shouldhelpgc = true 
  span = c.allocLarge(size, needzero, noscan) 
  ... 
 } 
 
 if shouldhelpgc { 
  if t := (gcTrigger{kind: gcTriggerHeap}); t.test() { 
   gcStart(t) 
  } 
 } 
 
 return x 
} 


小對象:如果申請小對象時,發現當前內存空間不存在空閑跨度時,將會需要調用 nextFree 方法獲取新的可用的對象,可能會觸發 GC 行為。

大對象:如果申請大於 32k 以上的大對象時,可能會觸發 GC 行為。

總結
在這篇文章中,我們介紹瞭 Go 語言觸發 GC 的兩大類場景,並分別基於大類中的細分場景進行瞭一一說明。

到此這篇關於談論Go 什麼時候會觸發 GC問題的文章就介紹到這瞭,更多相關Go 什麼時候會觸發 GC?內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: