golang源碼分析--gc

由於本人也屬於小白學習,學習過程中也有很多不解的地方,歡迎大家提問,或者指出我未能講到的部分,發現gc是一個很龐大的邏輯所以此篇會一直更新,到我覺得真的完全理解了再停更

golang概覽及原理

golang的垃圾回收採用的是 標記-清理(Mark-and-Sweep) 算法
就是先標記出需要回收的內存對象快,然後在清理掉;
選取三色標記清除法的原因:
1.對象整理的優勢是解決內存碎片問題以及“允許”使用順序內存分配器。但 Go 運行時的分配算法基於 tcmalloc,基本上沒有碎片問題。並且順序內存分配器在多線程的場景下並不適用。Go 使用的是基於 tcmalloc 的現代內存分配算法,對對象進行整理不會帶來實質性的性能提升。
2.分代 GC 依賴分代假設,即 GC 將主要的回收目標放在新創建的對象上(存活時間短,更傾向於被回收),而非頻繁檢查所有對象。但 Go 的編譯器會通過逃逸分析將大部分新生對象存儲在棧上(棧直接被回收),只有那些需要長期存在的對象纔會被分配到需要進行垃圾回收的堆中。也就是說,分代 GC 回收的那些存活時間短的對象在 Go 中是直接被分配到棧上,當 goroutine 死亡後棧也會被直接回收,不需要 GC 的參與,進而分代假設並沒有帶來直接優勢。並且 Go 的垃圾回收器與用戶代碼併發執行,使得 STW 的時間與對象的代際、對象的 size 沒有關係。Go 團隊更關注於如何更好地讓 GC 與用戶代碼併發執行(使用適當的 CPU 來執行垃圾回收),而非減少停頓時間這一單一目標上。
有了GC爲什麼還會內存泄漏:
原因:預期的能很快被釋放的內存由於附着在了長期存活的內存上,或生命期意外的被延長,導致預計能夠立即回收的內存長時間得不到回收(由於goroutine還有多種形式)
形式1:預期能被快速釋放的內存因被根對象引用而沒有得到迅速釋放:當有一個全局對象時,可能不經意間將某個變量附着在其上,且忽略的將其進行釋放,則該內存永遠不會得到釋放

var cache = map[interface{}]interface{}{}

func keepalloc() {
  for i := 0; i < 10000; i++ {
    m := make([]byte, 1<<10)
    cache[i] = m
  }
}

形式2:goroutine 泄漏:

Goroutine 作爲一種邏輯上理解的輕量級線程,需要維護執行用戶代碼的上下文信息。在運行過程中也需要消耗一定的內存來保存這類信息,而這些內存在目前版本的 Go 中是不會被釋放的。因此,如果一個程序持續不斷地產生新的 goroutine、且不結束已經創建的 goroutine 並複用這部分內存,就會造成內存泄漏的現象,


func keepalloc2() {
  for i := 0; i < 100000; i++ {
    go func() {
      select {}
    }()
  }
}

這種形式的 goroutine 泄漏還可能由 channel 泄漏導致。而 channel 的泄漏本質上與 goroutine 泄漏存在直接聯繫。Channel 作爲一種同步原語,會連接兩個不同的 goroutine,如果一個 goroutine 嘗試向一個沒有接收方的無緩衝 channel 發送消息,則該 goroutine 會被永久的休眠,整個 goroutine 及其執行棧都得不到釋放,


var ch = make(chan struct{})

func keepalloc3() {
  for i := 0; i < 100000; i++ {
    // 沒有接收方,goroutine 會一直阻塞
    go func() { ch <- struct{}{} }()
  }
}

gc的觸發條件:
1.主動觸發,通過調用 runtime.GC 來觸發 GC,此調用阻塞式地等待當前 GC 運行完畢。
2.被動觸發,分爲兩種方式:
2.1使用系統監控,當超過兩分鐘沒有產生任何 GC 時,強制觸發 GC。
2.2使用步調(Pacing)算法,其核心思想是控制內存增長的比例。
gc優化的手段
1.控制內存分配的速度,限制 goroutine 的數量,從而提高賦值器對 CPU 的利用率。
2.減少並複用內存,例如使用 sync.Pool 來複用需要頻繁創建臨時對象,例如提前分配足夠的內存來降低多餘的拷貝。
3.需要時,增大 GOGC 的值,降低 GC 的運行頻率。

根據源碼go 1.12.9註釋GC的流程如下:

gcphase的三個狀態
_GCoff // GC not running; sweeping in background, write barrier disabled
_GCmark // GC marking roots and workbufs: allocate black, write barrier ENABLED
_GCmarktermination // GC mark termination: allocate black, P’s help GC, write barrier ENABLED
1.GC執行掃描和終止
a. 暫停整個程序(stop the world),等待所有goroutine到達GC安全點(備註見定義)
b.清除任何未經清除的span,只有在預期時間之前強制執行此GC週期時,纔會有未清除的span。
2.GC的標記階段
a.爲了標記階段將gcphase從_GCoff設置成_GCmark,開啓寫屏障(詳情見備註),啓用mutator assist(有所疑問不知作用),將根標記任務放入隊列。通過STW保證沒有對象會被掃描,直到所有協程(Ps)啓用寫屏障。
b.喚醒程序(start the world),從此開始,GC的工作由調度程序啓用的mark workers和allocation一部分的assists performed執行,寫屏障將任何指針指向的新指針和覆蓋指針都標記爲灰,新申請的對象立即判爲黑色。
c.gc執行根標記任務(什麼是跟對象?見備註),這包括掃描所有的棧,爲所有全局變量標灰色,以及對堆外運行時數據結構中的任何堆指針進行標灰色。掃描棧會停止goroutine,爲goroutine指針指向的所有棧着灰色,然後再重啓goroutine
d.GC排出灰色對象的工作隊列,將每個灰色對象掃描爲黑色,並對在該對象中找到的所有指針標記爲灰色(這反過來又可能將這些指針添加到工作隊列中)。
e.由於 GC work 分散在本地緩存中,因此 GC 使用分佈式終止算法來檢測何時不再有根標記作業或灰色對象(參見gcMarkDone函數)。此時,GC 狀態轉換到標記終止(gcMarkTermination)。
3.GC執行標記終止
a.暫停程序(stop the world)
b.設置gcphase狀態到_GCmarktermination,停止workers和assists
c.清理工作,如回收mcaches內存
4. GC執行清除階段
a.設置gcphase到_GCoff,設置清除狀態
並禁止寫屏障。

b.喚醒程序(start the world),從此時開始,新申請的對象爲白色,若必要可以在使用前清掃spans。
c.gc在後臺執行回收白色對象並響應內存的分配
5.當內存分配足夠多,則從1再開始

golang中GC的觸發時機:

1.gcTriggerAlways: 強制觸發GC
2.gcTriggerHeap: 當前分配的內存達到一定閾值時觸發,這個閾值在每次GC過後都會根據堆內存的增長情況3.和CPU佔用率來調整
4.gcTriggerTime: 當一定時間沒有執行過GC就觸發GC(2分鐘)
5.gcTriggerCycle: runtime.GC()調用

//GC函數是用我們可以主動開啓GC的一個函數
//GC函數流程:一個流程,清除終結,標記,標記終結,清除
//1.在循環N中,清除終止,標記或者標記終止,請等待到標記終止過渡到清除N
//2.在清除N中,我們完成清除,在這個節點我們可以開啓第N+1個完整週期
//3.通過清除終結N+1來觸發N+1循環
//4.等待N+1的標記結束
//5.幫助清除N+1直到結束
func GC() {
    //獲取當前循環
	n := atomic.Load(&work.cycles)
	//gcWaitOnMark一直阻塞,直到GC完成第N個標記階段。 如果GC已完成此標記階段,它將立即返回。
	gcWaitOnMark(n)

	// 因爲gcphase狀態不爲_GCmark
	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源代碼,gcStart的代碼對應的是真正的gc流程

//開啓GC流程,將gcphase的狀態從_GCoff設置到_GCmark或者執行所有的GC流程
func gcStart(trigger gcTrigger) {
	// 檢查執行條件
	mp := acquirem()
	if gp := getg(); gp == mp.g0 || mp.locks > 1 || mp.preemptoff != "" {
		releasem(mp)
		return
	}
	releasem(mp)
	mp = nil
	//清除任何未經清除的span
	for trigger.test() && sweepone() != ^uintptr(0) {
		sweep.nbgsweep++
	}


	//開啓第一階段,通過work中的標記指代不同的階段
	semacquire(&work.startSema)
	// Re-check transition condition under transition lock.
	if !trigger.test() {
		semrelease(&work.startSema)
		return
	}
	// For stats, check if this GC was forced by the user.
	work.userForced = trigger.kind == gcTriggerAlways || trigger.kind == gcTriggerCycle
	// 根據當前gc的gcstoptheworld運行狀態的不同指定mode
	//目的是防止觸發器使得多個gorooutine啓用多個STW
	mode := gcBackgroundMode//
	if debug.gcstoptheworld == 1 {
		mode = gcForceMode
	} else if debug.gcstoptheworld == 2 {
		mode = gcForceBlockMode
	}
	//加鎖,STW的第一步
	semacquire(&worldsema)

	if trace.enabled {
		traceGCStart()
	}
	// 檢測所有goroutine已經到安全狀態並回收緩存
	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
   //gcBgMarkStartWorkers準備後臺標記工作進程goroutines。
   //這些goroutine在mark階段之前不會運行,但必須在工作未停止時從常規G堆棧啓動它們。呼叫者必須持有worldsema
   //特別注意,此函數後臺worker程序會執行gcphase狀態到_GCmarktermination,判斷標記結束
   //也會置gcphase到_GCoff,設置清除狀態並禁止寫屏障
	gcBgMarkStartWorkers()
    // gcResetMarkState重置標記之前的全局狀態(併發或STW),並重置所有G的堆棧掃描狀態。
    //這是安全的,無需停止世界,因爲在此期間或之後創建的所有G都會以重置狀態開始。 
	gcResetMarkState()

	work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocs
	if 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 = 0
	work.mode = mode

	now := nanotime()
	work.tSweepTerm = now
	work.pauseStart = now
	if trace.enabled {
		traceGCSTWStart(1)
	}
	//****************************************
	//STW的核心實現,到這裏爲止完成了STW
	//****************************************
	systemstack(stopTheWorldWithSema)
	// Finish sweep before we start concurrent scan.
	systemstack(func() {
		finishsweep_m()
	})
	// 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 {
		schedEnableUser(false)
	}
    //轉換gcphase的狀態,進入當前標記階段,並開啓寫屏障
 	setGCPhase(_GCmark)
 	//爲標記做準備
	gcBgMarkPrepare() // Must happen before assist enable.
	//gcMarkRootPrepare將根掃描作業(堆棧、全局和一些其他)排隊,並初始化掃描相關狀態。
	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()

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

	// Assists and workers can start the moment we start
	// the world.
	//statr world
	gcController.markStartTime = now

	// Concurrent mark.
	systemstack(func() {
		now = startTheWorldWithSema(trace.enabled)
		work.pauseNS += now - work.pauseStart
		work.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)
}

備註
安全點:程序執行期間的一個點,在此點上所有GC根都是已知的,並且所有堆對象內容都是一致的。從全局的角度來看,所有線程都必須在安全點阻塞,然後GC才能運行。
寫屏障:https://segmentfault.com/a/1190000012597428,此鏈接中有例子,將寫屏障講的十分準確
根對象:它是垃圾回收器在標記過程時最先檢查的對象,包括:
全局變量:程序在編譯期就能確定的那些存在於程序整個生命週期的變量。
執行棧:每個 goroutine 都包含自己的執行棧,這些執行棧上包含棧上的變量及指向分配的堆內存區塊的指針。
寄存器:寄存器的值可能表示一個指針,參與計算的這些指針可能指向某些賦值器分配的堆內存區塊。
tcmalloc:核心思想就是把內存分爲多級管理,從而降低鎖的粒度。它將可用的堆內存採用二級分配的方式進行管理:每個線程都會自行維護一個獨立的內存池,進行內存分配時優先從該內存池中分配,當內存池不足時纔會向全局內存池申請,以避免不同線程對全局內存池的頻繁競爭。

參考文章:
Go GC 20 問

發佈了210 篇原創文章 · 獲贊 33 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章