欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang內(nèi)存管理之垃圾收集器詳解

 更新時(shí)間:2023年06月30日 12:14:03   作者:IguoChan  
這篇文章我們主要介紹垃圾收集器的設(shè)計(jì)原理以及Golang垃圾收集器的實(shí)現(xiàn)原理,文中有詳細(xì)的代碼示例及圖文介紹,感興趣的小伙伴跟著小編一起來(lái)學(xué)習(xí)吧

0. 簡(jiǎn)介

C/C++等語(yǔ)言使用手動(dòng)的方式管理堆內(nèi)存不同,GoPython、Java使用自動(dòng)的內(nèi)存管理系統(tǒng),包括垃圾收集(Garbage Collection,縮寫GC)機(jī)制。下面,我們將介紹垃圾收集器的設(shè)計(jì)原理以及Golang垃圾收集器的實(shí)現(xiàn)原理。

1. 常見的GC算法

1.1 引用計(jì)數(shù)法

為每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù),當(dāng)引用對(duì)象銷毀時(shí),引用計(jì)數(shù)-1,當(dāng)對(duì)象的引用計(jì)數(shù)變?yōu)?后,就回收該對(duì)象。

  • 代表語(yǔ)言:Python、PHPSwift
  • 優(yōu)點(diǎn):對(duì)象回收快,簡(jiǎn)單直接;
  • 缺點(diǎn):不能很好地處理循環(huán)引用問(wèn)題;實(shí)時(shí)維護(hù)引用計(jì)數(shù)是有損耗的。

1.2 標(biāo)記-清除

從根變量開始遍歷所有的引用對(duì)象,標(biāo)記引用對(duì)象,沒(méi)有被標(biāo)記的對(duì)象進(jìn)行回收。

  • 代表語(yǔ)言:Golang;
  • 優(yōu)點(diǎn):解決了引用計(jì)數(shù)方式的缺點(diǎn),較為簡(jiǎn)單;
  • 缺點(diǎn):需要STW(Stop The World),影響性能;另外也有可能造成內(nèi)存碎片的問(wèn)題。

1.3 分代收集

按照對(duì)象生命周期長(zhǎng)短劃分不同的代空間,生命周期長(zhǎng)的放入老年代,短的放入新生代,不同代有不同的回收算法和回收頻率。

  • 代表語(yǔ)言:Java;
  • 優(yōu)點(diǎn):回收性能好;
  • 缺點(diǎn):算法復(fù)雜。

2. Golang GC原理

2.1 算法選擇

Golang的垃圾回收算法使用的是無(wú)分代、不整理、并發(fā)的三色標(biāo)記清除算法:

  • Go運(yùn)行時(shí)的內(nèi)存分配基于tcmalloc算法,基本上沒(méi)有碎片問(wèn)題,從而避免了標(biāo)記-清除算法中容易產(chǎn)生內(nèi)存碎片的問(wèn)題;
  • Go的垃圾回收器與用戶代碼并發(fā)執(zhí)行,提升GC效率,降低對(duì)用戶代碼的影響。

2.2 三色標(biāo)記

2.2.1 標(biāo)記-清除算法

最簡(jiǎn)單的標(biāo)記-清除算法中,分為標(biāo)記和清除階段。在掃描階段,從垃圾回收的根對(duì)象出發(fā),掃描整個(gè)引用鏈,找到所有可達(dá)對(duì)象進(jìn)行標(biāo)記。在清除階段,掃描所有的不可達(dá)對(duì)象,然后將垃圾對(duì)象清除掉。

但是該算法有一個(gè)很大的缺點(diǎn):整個(gè)過(guò)程必須STW(Stop The World)。這導(dǎo)致整個(gè)應(yīng)用程序必須停止,嚴(yán)重影響程序?qū)崟r(shí)性和效率。

2.2.2 三色標(biāo)記算法

為了解決原始標(biāo)記-清除帶來(lái)的長(zhǎng)時(shí)間的STW,多數(shù)現(xiàn)代的追蹤式垃圾收集器一般都會(huì)實(shí)現(xiàn)三色標(biāo)記算法以縮短STW的時(shí)間。三色標(biāo)記法將程序中的對(duì)象分為白色、黑色和灰色三類:

  • 白色對(duì)象:潛在的垃圾,其內(nèi)存可能會(huì)被垃圾收集器回收;
  • 黑色對(duì)象:活躍的對(duì)象,已經(jīng)被掃描過(guò)的對(duì)象;
  • 灰色對(duì)象:活躍的對(duì)象,剛好掃描到的對(duì)象,但是還需要對(duì)其子對(duì)象進(jìn)行掃描,因?yàn)榭赡艽嬖谥赶虬咨珜?duì)象。

三色標(biāo)記法的標(biāo)記過(guò)程如下:

  • 起初所有的對(duì)象都是白色的;
  • 從根對(duì)象出發(fā)掃描所有可達(dá)對(duì)象,標(biāo)記為灰色,放入灰色集合;
  • 從灰色集合中取出灰色對(duì)象,將其引用的對(duì)象標(biāo)記為灰色并放入到灰色集合中,自身標(biāo)記為黑色;
  • 重復(fù)步驟3,直到灰色集合為空,此時(shí)白色對(duì)象即為不可達(dá)的“垃圾”,回收白色對(duì)象。

根對(duì)象在垃圾回收的術(shù)語(yǔ)中又叫根集合,它是垃圾回收器在標(biāo)記過(guò)程中最先檢查的對(duì)象,包括:

  • 全局變量:程序在編譯時(shí)就能確定的那些在整個(gè)程序生命周期都將存活的變量;
  • 執(zhí)行棧:每個(gè)goroutine都有自己的執(zhí)行棧,這些執(zhí)行棧上依舊存活的棧對(duì)象以及指向分配的堆內(nèi)存的指針對(duì)象。
  • 寄存器:寄存器的值可能表示一個(gè)指針,參與計(jì)算的這些指針可能指向某個(gè)分配的內(nèi)存地址。

因?yàn)橛脩艨赡軙?huì)在標(biāo)記的過(guò)程中修改對(duì)象的指針,比如出現(xiàn)以下情形,在如下所示的三色標(biāo)記過(guò)程中,用戶程序建立了從 A 對(duì)象到 D 對(duì)象的引用,但是因?yàn)槌绦蛑幸呀?jīng)不存在灰色對(duì)象了,所以 D 對(duì)象會(huì)被垃圾收集器錯(cuò)誤地回收。

要想解決以上問(wèn)題,要么就和“標(biāo)記—清除”算法一樣,STW整個(gè)過(guò)程,但是這種方式會(huì)對(duì)用戶程序影響比較大,降低程序性能。

如果要GC和用戶程序并發(fā)執(zhí)行,且保證內(nèi)存安全,那么就需要使用屏障技術(shù)了。

2.2.3 屏障技術(shù)

內(nèi)存屏障技術(shù)是一種屏障指令,它可以讓 CPU 或者編譯器在執(zhí)行內(nèi)存相關(guān)操作時(shí)遵循特定的約束,目前多數(shù)的現(xiàn)代處理器都會(huì)亂序執(zhí)行指令以最大化性能,但是該技術(shù)能夠保證內(nèi)存操作的順序性,在內(nèi)存屏障前執(zhí)行的操作一定會(huì)先于內(nèi)存屏障后執(zhí)行的操作。

想要在并發(fā)和增量的標(biāo)記算法中保證正確性,我們需要滿足以下兩種三色不變性之一:

  • 強(qiáng)三色不變性:黑色對(duì)象不會(huì)指向白色對(duì)象,只會(huì)指向灰色或者黑色對(duì)象;
  • 弱三色不變性:黑色對(duì)象指向的白色對(duì)象必須包含一條從灰色對(duì)象經(jīng)由多個(gè)白色對(duì)象的可達(dá)路徑;

插入寫屏障

Dijkstra 于1978年提出的插入寫屏障,通過(guò)如下所示的算法,用戶程序和垃圾收集器可以在并行工作的情況下保證內(nèi)存安全:

// 灰色賦值器 Dijkstra 插入屏障
func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
    shade(ptr) //先將新下游對(duì)象 ptr 標(biāo)記為灰色
    *slot = ptr
}
//說(shuō)明:
添加下游對(duì)象(當(dāng)前下游對(duì)象slot, 新下游對(duì)象ptr) {   
  //step 1
  標(biāo)記灰色(新下游對(duì)象ptr)   
  //step 2
  當(dāng)前下游對(duì)象slot = 新下游對(duì)象ptr                    
}
//場(chǎng)景:
A.添加下游對(duì)象(nil, B)   //A 之前沒(méi)有下游, 新添加一個(gè)下游對(duì)象B, B被標(biāo)記為灰色
A.添加下游對(duì)象(C, B)     //A 將下游對(duì)象C 更換為B,  B被標(biāo)記為灰色

上述偽代碼很好理解,每當(dāng)執(zhí)行*slot = ptr表達(dá)式時(shí),我們會(huì)執(zhí)行上述寫屏障(通過(guò)shade)嘗試改變?cè)撝羔樀念伾绻撝羔樤臼前咨?,那么通過(guò)該函數(shù)將其設(shè)置為灰色,否則保持不變。

如上圖所示的標(biāo)記過(guò)程:

  • 垃圾收集器將根對(duì)象指向 A 對(duì)象標(biāo)記成黑色并將 A 對(duì)象指向的對(duì)象 B 標(biāo)記成灰色;
  • 用戶程序修改 A 對(duì)象的指針,將原本指向 B 對(duì)象的指針指向 C 對(duì)象,這時(shí)觸發(fā)寫屏障將 C 對(duì)象標(biāo)記成灰色;
  • 垃圾收集器依次遍歷程序中的其他灰色對(duì)象,將它們分別標(biāo)記成黑色;

插入寫屏障是一種相對(duì)保守的屏障技術(shù),它有以下兩個(gè)缺點(diǎn):

  • 在一次回收過(guò)程中可能會(huì)殘留一部分對(duì)象沒(méi)有回收成功,只有下一個(gè)回收過(guò)程中才會(huì)回收;
  • 棧對(duì)象在垃圾回收中也被認(rèn)為是根對(duì)象,為了保證內(nèi)存安全:
    • 為棧上的對(duì)象增加屏障:大幅增加寫入指針的額外開銷;
    • 重新對(duì)棧上對(duì)象進(jìn)行掃描:重新掃描棧對(duì)象需要STW;

刪除寫屏障

Yuasa 于1990年提出的刪除寫屏障的算法如下:

// 黑色賦值器 Yuasa 屏障  
func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {  
    shade(*slot) 先將*slot標(biāo)記為灰色  
    *slot = ptr  
}  
//說(shuō)明:  
添加下游對(duì)象(當(dāng)前下游對(duì)象slot, 新下游對(duì)象ptr) {  
    //step 1  
    if (當(dāng)前下游對(duì)象slot是灰色 || 當(dāng)前下游對(duì)象slot是白色) {  
        標(biāo)記灰色(當(dāng)前下游對(duì)象slot) //slot為被刪除對(duì)象, 標(biāo)記為灰色  
    }  
    //step 2  
    當(dāng)前下游對(duì)象slot = 新下游對(duì)象ptr  
}  
//場(chǎng)景  
A.添加下游對(duì)象(B, nil) //A對(duì)象,刪除B對(duì)象的引用。B被A刪除,被標(biāo)記為灰(如果B之前為白)  
A.添加下游對(duì)象(B, C) //A對(duì)象,更換下游B變成C。B被A刪除,被標(biāo)記為灰(如果B之前為白)

上述代碼會(huì)在老對(duì)象的引用被刪除時(shí),將白色的老對(duì)象涂成灰色,這樣刪除寫屏障就可以保證弱三色不變性,老對(duì)象引用的下游對(duì)象一定可以被灰色對(duì)象引用。

如上圖所示的標(biāo)記過(guò)程:

  • 垃圾收集器將根對(duì)象指向 A 對(duì)象標(biāo)記成黑色并將 A 對(duì)象指向的對(duì)象 B 標(biāo)記成灰色;
  • 用戶程序?qū)?A 對(duì)象原本指向 B 的指針指向 C,觸發(fā)刪除寫屏障,但是因?yàn)?B 對(duì)象已經(jīng)是灰色的,所以不做改變;
  • 用戶程序?qū)?B 對(duì)象原本指向 C 的指針刪除,觸發(fā)刪除寫屏障,白色的 C 對(duì)象被涂成灰色,避免發(fā)生懸掛指針以保證用戶程序的正確性;
  • 垃圾收集器依次遍歷程序中的其他灰色對(duì)象,將它們分別標(biāo)記成黑色;

混合寫屏障

分析以上兩種屏障方式,如果采用純粹的插入寫屏障,滿足強(qiáng)三色不變?cè)?,但是棧上的?duì)象不設(shè)置寫屏障的話會(huì)導(dǎo)致黑色的??赡苤赶虬咨亩?,所以必須STW重新掃描棧才能保證不丟對(duì)象,而在大量goroutine的環(huán)境下,STW的延遲不可控。

如果單純的使用刪除寫屏障,其基于其實(shí)快照的解決方案(snapshot-at-the-begining)。顧名思義,就是在開始 gc 之前,必須 STW ,對(duì)整個(gè)根做一次起始快照。當(dāng)賦值器(業(yè)務(wù)線程)從灰色或者白色對(duì)象中刪除白色指針時(shí)候,寫屏障會(huì)捕捉這一行為,將這一行為通知給回收器。

在Go v1.8版本引入了混合寫屏障,結(jié)合了二者的優(yōu)點(diǎn),極大地減少了STW的時(shí)間,提升系統(tǒng)性能。

混合寫屏障的具體操作如下:

  • GC開始時(shí)將棧上的可達(dá)對(duì)象全部掃描并且標(biāo)記為黑色(之后不再進(jìn)行第二次重復(fù)掃描,無(wú)需STW);
  • GC期間,任何在棧上創(chuàng)建的新對(duì)象,均為黑色;
  • 堆上被刪除的對(duì)象標(biāo)記為灰色;
  • 堆上新添加的對(duì)象標(biāo)記為灰色。

以下是個(gè)簡(jiǎn)單的流程,圖片來(lái)自于詳細(xì)總結(jié): Golang GC、三色標(biāo)記、混合寫屏障機(jī)制,侵刪!

其實(shí)總結(jié)起來(lái)就是,在GC期間:

  • 棧上可達(dá)對(duì)象都標(biāo)記為黑色,包括在此期間新創(chuàng)建的;
  • 堆上的對(duì)象則會(huì)觸發(fā)混合屏障機(jī)制,那么在機(jī)制生效后,即使有棧上黑色指向白色的堆對(duì)象,那也一定有一條從灰色堆對(duì)象到此白對(duì)象的可達(dá)路徑,符合弱三色不變?cè)怼?/li>

比如以下,就不會(huì)有棧對(duì)象能引用堆對(duì)象8,因?yàn)閳D中的8號(hào)顯然是不可達(dá)的,所以不會(huì)出現(xiàn)不滿足弱三色不變?cè)淼那樾?。那為什?號(hào)對(duì)象可以引用7號(hào)對(duì)象呢?這是因?yàn)?號(hào)對(duì)象在引用7號(hào)對(duì)象的時(shí)候,對(duì)象7是在對(duì)象6的下游,本身是可達(dá)。

總結(jié)下來(lái)就是,混合屏障結(jié)合了插入和刪除寫屏障的優(yōu)點(diǎn)

  • 棧上數(shù)據(jù)(存活可達(dá)的)直接置黑保證了各個(gè)goroutine棧無(wú)需多次掃描,優(yōu)化了空間;
  • 插入寫屏障保障了堆上的新增數(shù)據(jù)是灰色的;
  • 刪除寫屏障保障了堆上被刪除的數(shù)據(jù)是灰色的,避免黑色的棧上數(shù)據(jù)指向時(shí),其未變色被刪;

3. Golang GC過(guò)程

Golang垃圾收集的過(guò)程有以下四個(gè)階段:

  • GC開始(STW);
  • 并發(fā)掃描與輔助標(biāo)記;
  • 標(biāo)記終止;
  • 內(nèi)存清理。

3.1 GC開始(STW)

垃圾回收在啟動(dòng)時(shí)都會(huì)調(diào)用runtime.gcStart函數(shù):

func gcStart(trigger gcTrigger) {
   ...
   for trigger.test() && sweepone() != ^uintptr(0) {
      sweep.nbgsweep++
   }
   // Perform GC initialization and the sweep termination
   // transition.
   semacquire(&work.startSema)
   // Re-check transition condition under transition lock.
   if !trigger.test() {
      semrelease(&work.startSema)
      return
   }
   ...
}

首先檢查是否符合GC條件,在循環(huán)中驗(yàn)證收集條件的同時(shí)還會(huì)不斷調(diào)用runtime.sweepone清理已經(jīng)被標(biāo)記的內(nèi)存單元,完成上一個(gè)垃圾收集循環(huán)的收尾工作。

在下一小步之前,會(huì)再次check一下是否滿足GC條件。

接下來(lái),調(diào)用gcBgMarkStartWorkers啟動(dòng)后臺(tái)標(biāo)記任務(wù)、在系統(tǒng)棧中調(diào)用stopTheWorldWithSema暫停程序并調(diào)用finishsweep_m保證上一次GC的工作結(jié)束。

func gcStart(trigger gcTrigger) {
	...
	semacquire(&worldsema)
	gcBgMarkStartWorkers()
	work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocs
	...
	systemstack(stopTheWorldWithSema)
	systemstack(func() {
		finishsweep_m()
	})
	work.cycles++
	gcController.startCycle()
	...
}
func gcStart(trigger gcTrigger) {
	...
	setGCPhase(_GCmark)
	gcBgMarkPrepare()
	gcMarkRootPrepare()
	atomic.Store(&gcBlackenEnabled, 1)
	systemstack(func() {
		now = startTheWorldWithSema(trace.enabled)
		work.pauseNS += now - work.pauseStart
		work.tMark = now
	})
	semrelease(&work.startSema)
}

總結(jié)下來(lái),在GC開啟階段:

  • 需要STW暫停程序執(zhí)行;
  • 啟動(dòng)后臺(tái)標(biāo)記任務(wù),用于第二階段;
  • 啟動(dòng)寫屏障;
  • 將root根對(duì)象放入到標(biāo)記隊(duì)列(放入就是標(biāo)記為灰色);
  • 取消STW,進(jìn)入第二階段。

3.2 并發(fā)掃描與標(biāo)記輔助

前面說(shuō)過(guò),調(diào)用gcBgMarkStartWorkers啟動(dòng)后臺(tái)標(biāo)記任務(wù),該函數(shù)為每個(gè)處理器創(chuàng)建用于執(zhí)行后臺(tái)任務(wù)的

func gcBgMarkStartWorkers() {
   // Background marking is performed by per-P G's. Ensure that each P has
   // a background GC G.
   //
   // Worker Gs don't exit if gomaxprocs is reduced. If it is raised
   // again, we can reuse the old workers; no need to create new workers.
   for gcBgMarkWorkerCount < gomaxprocs {
      go gcBgMarkWorker()
      notetsleepg(&work.bgMarkReady, -1)
      noteclear(&work.bgMarkReady)
      // The worker is now guaranteed to be added to the pool before
      // its P's next findRunnableGCWorker.
      gcBgMarkWorkerCount++
   }
}
func gcBgMarkWorker() {
	gp := getg()
	gp.m.preemptoff = "GC worker init"
	node := new(gcBgMarkWorkerNode)
	gp.m.preemptoff = ""
	node.gp.set(gp)
	node.m.set(acquirem())
	notewakeup(&work.bgMarkReady)
	for {
		gopark(func(g *g, parkp unsafe.Pointer) bool {
			node := (*gcBgMarkWorkerNode)(nodep)
			if mp := node.m.ptr(); mp != nil {
				releasem(mp)
			}
			gcBgMarkWorkerPool.push(&node.node)
			return true
		}, unsafe.Pointer(node), waitReasonGCWorkerIdle, traceEvGoBlock, 0)
	...
}

喚醒后,我們根據(jù)處理器gcMarkWorkerMode 選擇不同的標(biāo)記執(zhí)行策略,不同的執(zhí)行策略都會(huì)調(diào)用gcDrain執(zhí)行掃描,這個(gè)函數(shù)可以作為分析Goalng三色著色的入口。

func gcBgMarkWorker() {
   ...
      // Preemption must not occur here, or another G might see
      // p.gcMarkWorkerMode.
      // Disable preemption so we can use the gcw. If the
      // scheduler wants to preempt us, we'll stop draining,
      // dispose the gcw, and then preempt.
      node.m.set(acquirem())
      pp := gp.m.p.ptr() // P can't change with preemption disabled.
      if gcBlackenEnabled == 0 {
         println("worker mode", pp.gcMarkWorkerMode)
         throw("gcBgMarkWorker: blackening not enabled")
      }
      if pp.gcMarkWorkerMode == gcMarkWorkerNotWorker {
         throw("gcBgMarkWorker: mode not set")
      }
      startTime := nanotime()
      pp.gcMarkWorkerStartTime = startTime
      decnwait := atomic.Xadd(&work.nwait, -1)
      if decnwait == work.nproc {
         println("runtime: work.nwait=", decnwait, "work.nproc=", work.nproc)
         throw("work.nwait was > work.nproc")
      }
      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 pp.gcMarkWorkerMode {
         default:
            throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
         case gcMarkWorkerDedicatedMode:
            gcDrain(&pp.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
            if gp.preempt {
               // We were preempted. This is
               // a useful signal to kick
               // everything out of the run
               // queue so it can run
               // somewhere else.
               if drainQ, n := runqdrain(pp); n > 0 {
                  lock(&sched.lock)
                  globrunqputbatch(&drainQ, int32(n))
                  unlock(&sched.lock)
               }
            }
            // Go back to draining, this time
            // without preemption.
            gcDrain(&pp.gcw, gcDrainFlushBgCredit)
         case gcMarkWorkerFractionalMode:
            gcDrain(&pp.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
         case gcMarkWorkerIdleMode:
            gcDrain(&pp.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
         }
         casgstatus(gp, _Gwaiting, _Grunning)
      })
      ...
}

當(dāng)所有的后臺(tái)工作任務(wù)都陷入等待并且沒(méi)有剩余工作時(shí),我們就認(rèn)為該輪垃圾收集的標(biāo)記階段結(jié)束了,然后調(diào)用gcMarkDone通知垃圾收集器。

func gcBgMarkWorker() {
   ...
       if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {
         // We don't need the P-local buffers here, allow
         // preemption because we may schedule like a regular
         // goroutine in gcMarkDone (block on locks, etc).
         releasem(node.m.ptr())
         node.m.set(nil)
         gcMarkDone()
      }
   }
}

標(biāo)記輔助

為了保證用戶程序分配內(nèi)存的速度不會(huì)超出后臺(tái)任務(wù)的標(biāo)記速度,運(yùn)行時(shí)還引入了標(biāo)記輔助技術(shù),它遵循一條非常簡(jiǎn)單并且樸實(shí)的原則,分配多少內(nèi)存就需要完成多少標(biāo)記任務(wù)。

3.3 標(biāo)記終止(STW)

func gcMarkDone() {
   ...
   systemstack(stopTheWorldWithSema)
   ...
   // Perform mark termination. This will restart the world.
   gcMarkTermination(nextTriggerRatio)
}

可以看到,進(jìn)入標(biāo)記終止階段之前會(huì)STW,然后在gcMarkTermination中會(huì)取消STW,所以此階段會(huì)取消STW,所以在此階段是會(huì)STW的。值得注意的是,在引入了混合寫屏障之后,即Go v1.8之后就不會(huì)在此階段對(duì)棧進(jìn)行re-scan了。

3.4 內(nèi)存清理

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

對(duì)于并行式清掃,在 GC 初始化的時(shí)候就會(huì)啟動(dòng) bgsweep(),然后在后臺(tái)一直循環(huán)。不管是阻塞式還是并行式,都是通過(guò) sweepone()函數(shù)來(lái)做清掃工作的。

func bgsweep(c chan int) {
    sweep.g = getg()
    lock(&sweep.lock)
    sweep.parked = true
    c <- 1
    goparkunlock(&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 = true
        goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
    }
}

GC觸發(fā)時(shí)機(jī)

后臺(tái)觸發(fā)

運(yùn)行時(shí)會(huì)在應(yīng)用程序啟動(dòng)時(shí)在后臺(tái)開啟一個(gè)用于強(qiáng)制觸發(fā)垃圾收集的 Goroutine,該 Goroutine 的職責(zé)非常簡(jiǎn)單 — 調(diào)用runtime.gcStart嘗試啟動(dòng)新一輪的垃圾收集:

func init() {
	go forcegchelper()
}
func forcegchelper() {
	forcegc.g = getg()
	for {
		lock(&forcegc.lock)
		atomic.Store(&forcegc.idle, 1)
		goparkunlock(&forcegc.lock, waitReasonForceGGIdle, traceEvGoBlock, 1)
		gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
	}
}

為了減少對(duì)計(jì)算資源的占用,該 Goroutine 會(huì)在循環(huán)中調(diào)用runtime.goparkunlock主動(dòng)陷入休眠等待其他 Goroutine 的喚醒,runtime.forcegchelper在大多數(shù)時(shí)間都是陷入休眠的,但是它會(huì)被系統(tǒng)監(jiān)控器runtime.sysmon在滿足垃圾收集條件時(shí)喚醒:

func sysmon() {
	...
	for {
		...
		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)
		}
	}
}

手動(dòng)觸發(fā)

用戶程序會(huì)通過(guò)runtime.GC函數(shù)在程序運(yùn)行期間主動(dòng)通知運(yùn)行時(shí)執(zhí)行,該方法在調(diào)用時(shí)會(huì)阻塞調(diào)用方直到當(dāng)前垃圾收集循環(huán)完成

以上就是Golang內(nèi)存管理之垃圾收集器詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang垃圾收集器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go使用Viper庫(kù)讀取YAML配置文件的示例代碼

    Go使用Viper庫(kù)讀取YAML配置文件的示例代碼

    Viper是適用于Go應(yīng)用程序的完整配置解決方案,它被設(shè)計(jì)用于在應(yīng)用程序中工作,并且可以處理所有類型的配置需求和格式,本文給大家介紹了Go使用Viper庫(kù)讀取YAML配置文件的方法,需要的朋友可以參考下
    2024-05-05
  • Go語(yǔ)言開發(fā)k8s之Service操作解析

    Go語(yǔ)言開發(fā)k8s之Service操作解析

    這篇文章主要為大家介紹了Go語(yǔ)言開發(fā)k8s之Service操作解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Golang請(qǐng)求fasthttp實(shí)踐

    Golang請(qǐng)求fasthttp實(shí)踐

    本文主要介紹了Golang請(qǐng)求fasthttp實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • golang讀取http的body時(shí)遇到的坑及解決

    golang讀取http的body時(shí)遇到的坑及解決

    這篇文章主要介紹了golang讀取http的body時(shí)遇到的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • golang獲取用戶輸入的幾種方式

    golang獲取用戶輸入的幾種方式

    這篇文章給大家介紹了golang獲取用戶輸入的幾種方式,文中通過(guò)代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友跟著小編一起來(lái)學(xué)習(xí)吧
    2024-01-01
  • Golang中 import cycle not allowed 問(wèn)題的解決方法

    Golang中 import cycle not allowed 問(wèn)題

    這篇文章主要介紹了Golang中 import cycle not allowed 問(wèn)題的解決方法,問(wèn)題從描述到解決都非常詳細(xì),需要的小伙伴可以參考一下
    2022-03-03
  • Golang單元測(cè)試與斷言編寫流程詳解

    Golang單元測(cè)試與斷言編寫流程詳解

    這篇文章主要介紹了Golang單元測(cè)試與斷言編寫流程,單元測(cè)試也是一個(gè)很重要的事情。單元測(cè)試是指在開發(fā)中,對(duì)一個(gè)函數(shù)或模塊的測(cè)試。其強(qiáng)調(diào)的是對(duì)單元進(jìn)行測(cè)試
    2022-12-12
  • golang通過(guò)遞歸遍歷生成樹狀結(jié)構(gòu)的操作

    golang通過(guò)遞歸遍歷生成樹狀結(jié)構(gòu)的操作

    這篇文章主要介紹了golang通過(guò)遞歸遍歷生成樹狀結(jié)構(gòu)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-04-04
  • golang配制高性能sql.DB的使用

    golang配制高性能sql.DB的使用

    本文主要講述SetMaxOpenConns(),?SetMaxIdleConns()?和?SetConnMaxLifetime()方法,?您可以使用它們來(lái)配置sql.DB的行為并改變其性能,感興趣的可以了解一下
    2021-12-12
  • 揭秘Go語(yǔ)言中的反射機(jī)制

    揭秘Go語(yǔ)言中的反射機(jī)制

    在Go語(yǔ)言中,反射是通過(guò)reflect包來(lái)實(shí)現(xiàn)的,通過(guò)使用反射,我們可以在運(yùn)行時(shí)獲取對(duì)象的類型信息、訪問(wèn)對(duì)象的字段和方法、動(dòng)態(tài)調(diào)用方法等,反射在很多場(chǎng)景下都非常有用,比如編寫通用的代碼、實(shí)現(xiàn)對(duì)象的序列化和反序列化、實(shí)現(xiàn)依賴注入等,需要的朋友可以參考下
    2023-10-10

最新評(píng)論