日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Golang垃圾回收机制(一)

發布時間:2024/9/5 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Golang垃圾回收机制(一) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文: http://legendtkl.com/2017/04/28/golang-gc/

1. Golang GC 發展

Golang 從第一個版本以來,GC 一直是大家詬病最多的。但是每一個版本的發布基本都伴隨著 GC 的改進。下面列出一些比較重要的改動。

v1.1 STW
v1.3 Mark STW, Sweep 并行
v1.5 三色標記法
v1.8 hybrid write barrier

2. GC 算法簡介

這一小節介紹三種經典的 GC 算法:引用計數(reference counting) 標記-清掃(mark & sweep)、節點復制(Copying Garbage Collection)分代收集(Generational Garbage Collection)。

2.1 引用計數

引用計數的思想非常簡單:每個單元維護一個域,保存其它單元指向它的引用數量(類似有向圖的入度)。當引用數量為 0 時,將其回收。引用計數是漸進式的,能夠將內存管理的開銷分布到整個程序之中。C++ 的 share_ptr 使用的就是引用計算方法。

引用計數算法實現一般是把所有的單元放在一個單元池里,比如類似 free list。這樣所有的單元就被串起來了,就可以進行引用計數了。新分配的單元計數值被設置為 1(注意不是 0,因為申請一般都說 ptr = new object 這種)。每次有一個指針被設為指向該單元時,該單元的計數值加 1;而每次刪除某個指向它的指針時,它的計數值減 1。當其引用計數為 0 的時候,該單元會被進行回收。雖然這里說的比較簡單,實現的時候還是有很多細節需要考慮,比如刪除某個單元的時候,那么它指向的所有單元都需要對引用計數減 1。那么如果這個時候,發現其中某個指向的單元的引用計數又為 0,那么是遞歸的進行還是采用其他的策略呢?遞歸處理的話會導致系統顛簸。關于這些細節這里就不討論了,可以參考文章后面的給的參考資料。

優點

  • 漸進式。內存管理與用戶程序的執行交織在一起,將 GC 的代價分散到整個程序。不像標記-清掃算法需要 STW (Stop The World,GC 的時候掛起用戶程序)。
  • 算法易于實現。
  • 內存單元能夠很快被回收。相比于其他垃圾回收算法,堆被耗盡或者達到某個閾值才會進行垃圾回收。

缺點

  • 原始的引用計數不能處理循環引用。大概這是被詬病最多的缺點了。不過針對這個問題,也除了很多解決方案,比如強引用等。
  • 維護引用計數降低運行效率。內存單元的更新刪除等都需要維護相關的內存單元的引用計數,相比于一些追蹤式的垃圾回收算法并不需要這些代價。
  • 單元池 free list 實現的話不是 cache-friendly 的,這樣會導致頻繁的 cache miss,降低程序運行效率。

2.2 標記-清掃

標記-清掃算法是第一種自動內存管理,基于追蹤的垃圾收集算法。算法思想在 70 年代就提出了,是一種非常古老的算法。內存單元并不會在變成垃圾立刻回收,而是保持不可達狀態,直到到達某個閾值或者固定時間長度。這個時候系統會掛起用戶程序,也就是 STW,轉而執行垃圾回收程序。垃圾回收程序對所有的存活單元進行一次全局遍歷確定哪些單元可以回收。算法分兩個部分:標記(mark)和清掃(sweep)。標記階段表明所有的存活單元,清掃階段將垃圾單元回收。可視化可以參考下圖。

標記-清掃算法的優點也就是基于追蹤的垃圾回收算法具有的優點:避免了引用計數算法的缺點(不能處理循環引用,需要維護指針)。缺點也很明顯,需要 STW。

三色標記算法

三色標記算法是對標記階段的改進,原理如下:

起初所有對象都是白色。
從根出發掃描所有可達對象,標記為灰色,放入待處理隊列。
從隊列取出灰色對象,將其引用對象標記為灰色放入隊列,自身標記為黑色。
重復 3,直到灰色對象隊列為空。此時白色對象即為垃圾,進行回收。
可視化如下。

三色標記的一個明顯好處是能夠讓用戶程序和 mark 并發的進行,具體可以參考論文:《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 實現也是基于這篇論文,后面再具體說明。

2.3 節點復制

節點復制也是基于追蹤的算法。其將整個堆等分為兩個半區(semi-space),一個包含現有數據,另一個包含已被廢棄的數據。節點復制式垃圾收集從切換(flip)兩個半區的角色開始,然后收集器在老的半區,也就是 Fromspace 中遍歷存活的數據結構,在第一次訪問某個單元時把它復制到新半區,也就是 Tospace 中去。在 Fromspace 中所有存活單元都被訪問過之后,收集器在 Tospace 中建立一個存活數據結構的副本,用戶程序可以重新開始運行了。

優點
所有存活的數據結構都縮并地排列在 Tospace 的底部,這樣就不會存在內存碎片的問題。
獲取新內存可以簡單地通過遞增自由空間指針來實現。
缺點
內存得不到充分利用,總有一半的內存空間處于浪費狀態。

2.4 分代收集

基于追蹤的垃圾回收算法(標記-清掃、節點復制)一個主要問題是在生命周期較長的對象上浪費時間(長生命周期的對象是不需要頻繁掃描的)。同時,內存分配存在這么一個事實 “most object die young”?;谶@兩點,分代垃圾回收算法將對象按生命周期長短存放到堆上的兩個(或者更多)區域,這些區域就是分代(generation)。對于新生代的區域的垃圾回收頻率要明顯高于老年代區域。

分配對象的時候從新生代里面分配,如果后面發現對象的生命周期較長,則將其移到老年代,這個過程叫做 promote。隨著不斷 promote,最后新生代的大小在整個堆的占用比例不會特別大。收集的時候集中主要精力在新生代就會相對來說效率更高,STW 時間也會更短。

優點
性能更優。

缺點
實現復雜

3. Golang GC

3.1 Overview

在說 Golang 的具體垃圾回收流程時,我們先來看一下幾個基本的問題。

1. 何時觸發GC

在堆上分配大于 32K byte 對象的時候進行檢測此時是否滿足垃圾回收條件,如果滿足則進行垃圾回收。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...shouldhelpgc := false// 分配的對象小于 32K byteif 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) }

2. GC 觸發條件

觸發條件主要關注下面代碼中的中間部分:forceTrigger || memstats.heap_live >= memstats.gc_trigger 。forceTrigger 是 forceGC 的標志;后面半句的意思是當前堆上的活躍對象大于我們初始化時候設置的 GC 觸發閾值。在 malloc 以及 free 的時候 heap_live 會一直進行更新,這里就不再展開了。

// gcShouldStart returns true if the exit condition for the _GCoff // phase has been met. The exit condition should be tested when // allocating. // // If forceTrigger is true, it ignores the current heap size, but // checks all other conditions. In general this should be false. func gcShouldStart(forceTrigger bool) bool {return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0 }//初始化的時候設置 GC 的觸發閾值 func gcinit() {_ = setGCPercent(readgogc())memstats.gc_trigger = heapminimum... } // 啟動的時候通過 GOGC 傳遞百分比 x // 觸發閾值等于 x * defaultHeapMinimum (defaultHeapMinimum 默認是 4M) func readgogc() int32 {p := gogetenv("GOGC")if p == "off" {return -1}if n, ok := atoi32(p); ok {return n}return 100 }

3. 垃圾回收的主要流程

三色標記法,主要流程如下:

所有對象最開始都是白色。
從 root 開始找到所有可達對象,標記為灰色,放入待處理隊列。
遍歷灰色對象隊列,將其引用對象標記為灰色放入待處理隊列,自身標記為黑色。
處理完灰色對象隊列,執行清掃工作。
詳細的過程如下圖所示,具體可參考 [9]。

關于上圖有幾點需要說明的是。

首先從 root 開始遍歷,root 包括全局指針和 goroutine 棧上的指針。
mark 有兩個過程。
從 root 開始遍歷,標記為灰色。遍歷灰色隊列。
re-scan 全局指針和棧。因為 mark 和用戶程序是并行的,所以在過程 1 的時候可能會有新的對象分配,這個時候就需要通過寫屏障(write barrier)記錄下來。re-scan 再完成檢查一下。
Stop The World 有兩個過程。
第一個是 GC 將要開始的時候,這個時候主要是一些準備工作,比如 enable write barrier。
第二個過程就是上面提到的 re-scan 過程。如果這個時候沒有 stw,那么 mark 將無休止。
另外針對上圖各個階段對應 GCPhase 如下:

Off: _GCoff Stack scan ~ Mark: _GCmark Mark termination: _GCmarktermination

3.2 寫屏障 (write barrier)

關于 write barrier,完全可以另外寫成一篇文章,所以這里只簡單介紹一下,這篇文章的重點還是 Golang 的 GC。垃圾回收中的 write barrier 可以理解為編譯器在寫操作時特意插入的一段代碼,對應的還有 read barrier。

為什么需要 write barrier,很簡單,對于和用戶程序并發運行的垃圾回收算法,用戶程序會一直修改內存,所以需要記錄下來。

Golang 1.7 之前的 write barrier 使用的經典的 Dijkstra-style insertion write barrier [Dijkstra ‘78], STW 的主要耗時就在 stack re-scan 的過程。自 1.8 之后采用一種混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])來避免 re-scan。具體的可以參考 17503-eliminate-rescan。

3.3 標記

下面的源碼還是基于 go1.8rc3。這個版本的 GC 代碼相比之前改動還是挺大的,我們下面盡量只關注主流程。垃圾回收的代碼主要集中在函數 gcStart() 中。

// gcStart 是 GC 的入口函數,根據 gcMode 做處理。 // 1. gcMode == gcBackgroundMode(后臺運行,也就是并行), _GCoff -> _GCmark // 2. 否則 GCoff -> _GCmarktermination,這個時候就是主動 GC func gcStart(mode gcMode, forceTrigger bool) {... }

1. STW phase 1

在 GC 開始之前的準備工作。

func gcStart(mode gcMode, forceTrigger bool) {...//在后臺啟動 mark worker if mode == gcBackgroundMode {gcBgMarkStartWorkers()}...// Stop The Worldsystemstack(stopTheWorldWithSema)...if mode == gcBackgroundMode {// GC 開始前的準備工作//處理設置 GCPhase,setGCPhase 還會 enable write barriersetGCPhase(_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 Worldsystemstack(startTheWorldWithSema)} else {...} }

2. Mark

Mark 階段是并行的運行,通過在后臺一直運行 mark worker 來實現。

func gcStart(mode gcMode, forceTrigger bool) {...//在后臺啟動 mark worker if mode == gcBackgroundMode {gcBgMarkStartWorkers()} }func gcBgMarkStartWorkers() {// Background marking is performed by per-P G's. Ensure that// each P has a background GC G.for _, p := range &allp {if p == nil || p.status == _Pdead {break}if p.gcBgMarkWorker == 0 {go gcBgMarkWorker(p)notetsleepg(&work.bgMarkReady, -1)noteclear(&work.bgMarkReady)}} } // gcBgMarkWorker 是一直在后臺運行的,大部分時候是休眠狀態,通過 gcController 來調度 func gcBgMarkWorker(_p_ *p) {for {// 將當前 goroutine 休眠,直到滿足某些條件gopark(...)...// 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)})...} } Mark 階段的標記代碼主要在函數 gcDrain() 中實現。// 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) - 1if job >= work.markrootJobs {break}markroot(gcw, job)if idle && pollWork() {goto done}}}// 處理 heap 標記// Drain heap marking jobs.for !(preemptible && gp.preempt) {...//從灰色列隊中取出對象var b uintptrif 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 phase 2)

mark termination 階段會 stop the world。函數實現在 gcMarkTermination()。1.8 版本已經不會再對 goroutine stack 進行 re-scan 了。細節有點多,這里不細說了。

func gcMarkTermination() {// World is stopped.// 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.})... }

3.4 清掃

清掃相對來說就簡單很多了。

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 = falseready(sweep.g, 0, true)}unlock(&sweep.lock) } 對于并行式清掃,在 GC 初始化的時候就會啟動 bgsweep(),然后在后臺一直循環。func bgsweep(c chan int) {sweep.g = getg()lock(&sweep.lock)sweep.parked = truec <- 1goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)for {for gosweepone() != ^uintptr(0) {sweep.nbgsweep++Gosched()}lock(&sweep.lock)if !gosweepdone() {// This can happen if a GC runs between// gosweepone returning ^0 above// and the lock being acquired.unlock(&sweep.lock)continue}sweep.parked = truegoparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)} }func gosweepone() uintptr {var ret uintptrsystemstack(func() {ret = sweepone()})return ret }

不管是阻塞式還是并行式,都是通過 sweepone()函數來做清掃工作的。如果對于上篇文章 Golang 內存管理 熟悉的話,這個地方就很好理解。內存管理都是基于 span 的,mheap_ 是一個全局的變量,所有分配的對象都會記錄在 mheap_ 中。在標記的時候,我們只要找到對對象對應的 span 進行標記,清掃的時候掃描 span,沒有標記的 span 就可以回收了。

// sweeps one span // returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep func sweepone() uintptr {...for {s := mheap_.sweepSpans[1-sg/2%2].pop()...if !s.sweep(false) {// 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}} }// Sweep frees or collects finalizers for blocks not marked in the mark phase. // It clears the mark bits in preparation for the next GC round. // Returns true if the span was returned to heap. // If preserve=true, don't return it to heap nor relink in MCentral lists; // caller takes care of it. func (s *mspan) sweep(preserve bool) bool {... }

3.5 其他

1. gcWork

這里介紹一下任務隊列,或者說灰色對象管理。每個 P 上都有一個 gcw 用來管理灰色對象(get 和 put),gcw 的結構就是 gcWork。gcWork 中的核心是 wbuf1 和 wbuf2,里面存儲就是灰色對象,或者說是 work(下面就全部統一叫做 work)。

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 }

既然每個 P 上有一個 work buffer,那么是不是還有一個全局的 work list 呢?是的。通過在每個 P 上綁定一個 work buffer 的好處和 cache 一樣,不需要加鎖。

var work struct {full uint64 // lock-free list of full blocks workbufempty uint64 // lock-free list of empty blocks workbufpad0 [sys.CacheLineSize]uint8 // prevents false-sharing between full/empty and nproc/nwait... }

那么為什么使用兩個 work buffer (wbuf1 和 wbuf2)呢?我下面舉個例子。比如我現在要 get 一個 work 出來,先從 wbuf1 中取,wbuf1 為空的話則與 wbuf2 swap 再 get。在其他時間將 work buffer 中的 full 或者 empty buffer 移到 global 的 work 中。這樣的好處在于,在 get 的時候去全局的 work 里面取(多個 goroutine 去取會有競爭)。這里有趣的是 global 的 work list 是 lock-free 的,通過原子操作 cas 等實現。下面列舉幾個函數看一下 gcWrok。

初始化

func (w *gcWork) init() {w.wbuf1 = wbufptrOf(getempty())wbuf2 := trygetfull()if wbuf2 == nil {wbuf2 = getempty()}w.wbuf2 = wbufptrOf(wbuf2) }

put

// put enqueues a pointer for the garbage collector to trace. // obj must point to the beginning of a heap object or an oblet. func (w *gcWork) put(obj uintptr) {wbuf := w.wbuf1.ptr()if wbuf == nil {w.init()wbuf = w.wbuf1.ptr()// wbuf is empty at this point.} else if wbuf.nobj == len(wbuf.obj) {w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1wbuf = w.wbuf1.ptr()if wbuf.nobj == len(wbuf.obj) {putfull(wbuf)wbuf = getempty()w.wbuf1 = wbufptrOf(wbuf)flushed = true}}wbuf.obj[wbuf.nobj] = objwbuf.nobj++ }

get

// get dequeues a pointer for the garbage collector to trace, blocking // if necessary to ensure all pointers from all queues and caches have // been retrieved. get returns 0 if there are no pointers remaining. //go:nowritebarrier func (w *gcWork) get() uintptr {wbuf := w.wbuf1.ptr()if wbuf == nil {w.init()wbuf = w.wbuf1.ptr()// wbuf is empty at this point.}if wbuf.nobj == 0 {w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1wbuf = w.wbuf1.ptr()if wbuf.nobj == 0 {owbuf := wbufwbuf = getfull()if wbuf == nil {return 0}putempty(owbuf)w.wbuf1 = wbufptrOf(wbuf)}}// TODO: This might be a good place to add prefetch codewbuf.nobj--return wbuf.obj[wbuf.nobj] }

2. forcegc

我們上面講了兩種 GC 觸發方式:自動檢測和用戶主動調用。除此之后 Golang 本身還會對運行狀態進行監控,如果超過兩分鐘沒有 GC,則觸發 GC。監控函數是 sysmon(),在主 goroutine 中啟動。

// The main goroutine func main() {...systemstack(func() {newm(sysmon, nil)}) } // Always runs without a P, so write barriers are not allowed. func sysmon() {...for {now := nanotime()unixnow := unixnanotime()lastgc := int64(atomic.Load64(&memstats.last_gc))if gcphase == _GCoff && lastgc != 0 && unixnow-lastgc > forcegcperiod && atomic.Load(&forcegc.idle) != 0 {lock(&forcegc.lock)forcegc.idle = 0forcegc.g.schedlink = 0injectglist(forcegc.g) // 將 forcegc goroutine 加入 runnable queueunlock(&forcegc.lock)}} }var forcegcperiod int64 = 2 * 60 *1e9 //兩分鐘

4.參考資料

《Go 語言學習筆記》
《垃圾收集》 - 豆瓣
Tracing Garbage Collection - wikipedia
《On-the-fly garbage collection: an exercise in cooperation.》 — Edsger W. Dijkstra, Leslie Lamport, A. J. Martin
Garbage Collection
Tracing Garbage Collection
Copying Garbage Collection – youtube
Generational Garbage Collection – youtube
golang gc talk
17503-eliminate-rescan

轉載于:https://www.cnblogs.com/informatics/p/10164321.html

總結

以上是生活随笔為你收集整理的Golang垃圾回收机制(一)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。