Golang采用了三色標(biāo)記法來(lái)進(jìn)行垃圾回收,那么在什么場(chǎng)景下會(huì)觸發(fā)這個(gè)回收動(dòng)作呢?
10年積累的成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè)經(jīng)驗(yàn),可以快速應(yīng)對(duì)客戶對(duì)網(wǎng)站的新想法和需求。提供各種問(wèn)題對(duì)應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識(shí)你,你也不認(rèn)識(shí)我。但先網(wǎng)站制作后付款的網(wǎng)站建設(shè)流程,更有蒲縣免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
源碼主要位于文件 src/runtime/mgc.go go version 1.16
觸發(fā)條件從大方面說(shuō),可分為 手動(dòng)觸發(fā) 和 系統(tǒng)觸發(fā) 兩種方式。手動(dòng)觸發(fā)一般很少用,主要由開(kāi)發(fā)者通過(guò)調(diào)用 runtime.GC() 函數(shù)來(lái)實(shí)現(xiàn),而對(duì)于系統(tǒng)自動(dòng)觸發(fā)是 運(yùn)行時(shí) 根據(jù)一些條件判斷來(lái)進(jìn)行的,這也正是本文要介紹的內(nèi)容。
不管哪種觸發(fā)方式,底層回收機(jī)制是一樣的,所以我們先看一下手動(dòng)觸發(fā),根據(jù)它來(lái)找系統(tǒng)觸發(fā)的條件。
可以看到開(kāi)始執(zhí)行GC的是 gcStart() 函數(shù),它有一個(gè) gcTrigger 參數(shù),是一個(gè)觸發(fā)條件結(jié)構(gòu)體,它的結(jié)構(gòu)體也很簡(jiǎn)單。
其實(shí)在Golang 內(nèi)部所有的GC都是通過(guò) gcStart() 函數(shù),然后指定一個(gè) gcTrigger 的參數(shù)來(lái)開(kāi)始的,而手動(dòng)觸發(fā)指定的條件值為 gcTriggerCycle 。 gcStart 是一個(gè)很復(fù)雜的函數(shù),有興趣的可以看一下源碼實(shí)現(xiàn)。
對(duì)于 kind 的值有三種,分別為 gcTriggerHeap 、 gcTriggerTime 和 gcTriggerCycle 。
運(yùn)行時(shí)會(huì)通過(guò) gcTrigger.test() 函數(shù)來(lái)決定是否需要觸發(fā)GC,只要滿足上面基中一個(gè)即可。
到此我們基本明白了這三種觸發(fā)GC的條件,那么對(duì)于系統(tǒng)自動(dòng)觸發(fā)這種,Golang 從一個(gè)程序的開(kāi)始到運(yùn)行,它又是如何一步一步監(jiān)控到這個(gè)條件的呢?
其實(shí) runtime 在程序啟動(dòng)時(shí),會(huì)在一個(gè)初始化函數(shù) init() 里啟用一個(gè) forcegchelper() 函數(shù),這個(gè)函數(shù)位于 proc.go 文件。
為了減少系統(tǒng)資源占用,在 forcegchelper 函數(shù)里會(huì)通過(guò) goparkunlock() 函數(shù)主動(dòng)讓自己陷入休眠,以后由 sysmon() 監(jiān)控線程根據(jù)條件來(lái)恢復(fù)這個(gè)gc goroutine。
可以看到 sysmon() 會(huì)在一個(gè) for 語(yǔ)句里一直判斷這個(gè) gcTriggerTime 這個(gè)條件是否滿足,如果滿足的話,會(huì)將 forcegc.g 這個(gè) goroutine 添加到全局隊(duì)列里進(jìn)行調(diào)度(這里 forcegc 是一個(gè)全局變量)。
調(diào)度器在調(diào)度循環(huán) runtime.schedule 中還可以通過(guò)垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 獲取并執(zhí)行用于后臺(tái)標(biāo)記的任務(wù)。
Golang的內(nèi)存分配是由golang runtime完成,其內(nèi)存分配方案借鑒自tcmalloc。
主要特點(diǎn)就是
本文中的element指一定大小的內(nèi)存塊是內(nèi)存分配的概念,并為出現(xiàn)在golang runtime源碼中
本文講述x8664架構(gòu)下的內(nèi)存分配
Golang 內(nèi)存分配有下面幾個(gè)主要結(jié)構(gòu)
Tiny對(duì)象是指內(nèi)存尺寸小于16B的對(duì)象,這類對(duì)象的分配使用mcache的tiny區(qū)域進(jìn)行分配。當(dāng)tiny區(qū)域空間耗盡時(shí)刻,它會(huì)從mcache.alloc[tinySpanClass]指向的mspan中找到空閑的區(qū)域。當(dāng)然如果mcache中span空間也耗盡,它會(huì)觸發(fā)從mcentral補(bǔ)充mspan到mcache的流程。
小對(duì)象是指對(duì)象尺寸在(16B,32KB]之間的對(duì)象,這類對(duì)象的分配原則是:
1、首先根據(jù)對(duì)象尺寸將對(duì)象歸為某個(gè)SpanClass上,這個(gè)SpanClass上所有的element都是一個(gè)統(tǒng)一的尺寸。
2、從mcache.alloc[SpanClass]找到mspan,看看有無(wú)空閑的element,如果有分配成功。如果沒(méi)有繼續(xù)。
3、從mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合適的mspan,返回給mcache。如果沒(méi)有找到就進(jìn)入mcentral.grow()—mheap.alloc()分配新的mspan給mcentral。
大對(duì)象指尺寸超出32KB的對(duì)象,此時(shí)直接從mheap中分配,不會(huì)走mcache和mcentral,直接走mheap.alloc()分配一個(gè)SpanClass==0 的mspan表示這部分分配空間。
對(duì)于程序分配常用的tiny和小對(duì)象的分配,可以通過(guò)無(wú)鎖的mcache提升分配性能。mcache不足時(shí)刻會(huì)拿mcentral的鎖,然后從mcentral中充mspan 給mcache。大對(duì)象直接從mheap 中分配。
在x8664環(huán)境上,golang管理的有效的程序虛擬地址空間實(shí)質(zhì)上只有48位。在mheap中有一個(gè)pages pageAlloc成員用于管理golang堆內(nèi)存的地址空間。golang從os中申請(qǐng)地址空間給自己管理,地址空間申請(qǐng)下來(lái)以后,golang會(huì)將地址空間根據(jù)實(shí)際使用情況標(biāo)記為free或者alloc。如果地址空間被分配給mspan或大對(duì)象后,那么被標(biāo)記為alloc,反之就是free。
Golang認(rèn)為地址空間有以下4種狀態(tài):
Golang同時(shí)定義了下面幾個(gè)地址空間操作函數(shù):
在mheap結(jié)構(gòu)中,有一個(gè)名為pages成員,它用于golang 堆使用虛擬地址空間進(jìn)行管理。其類型為pageAlloc
pageAlloc 結(jié)構(gòu)表示的golang 堆的所有地址空間。其中最重要的成員有兩個(gè):
在golang的gc流程中會(huì)將未使用的對(duì)象標(biāo)記為未使用,但是這些對(duì)象所使用的地址空間并未交還給os。地址空間的申請(qǐng)和釋放都是以golang的page為單位(實(shí)際以chunk為單位)進(jìn)行的。sweep的最終結(jié)果只是將某個(gè)地址空間標(biāo)記可被分配,并未真正釋放地址空間給os,真正釋放是后文的scavenge過(guò)程。
在gc mark結(jié)束以后會(huì)使用sweep()去嘗試free一個(gè)span;在mheap.alloc 申請(qǐng)mspan時(shí)刻,也使用sweep去清掃一下。
清掃mspan主要涉及到下面函數(shù)
如上節(jié)所述,sweep只是將page標(biāo)記為可分配,但是并未把地址空間釋放;真正的地址空間釋放是scavenge過(guò)程。
真正的scavenge是由pageAlloc.scavenge()—sysUnused()將掃描到待釋放的chunk所表示的地址空間釋放掉(使用sysUnused()將地址空間還給os)
golang的scavenge過(guò)程有兩種:
GC 與 mutator 線程并發(fā)運(yùn)行,允許多個(gè) GC 線程并行運(yùn)行
GC 是一個(gè)使用寫(xiě)屏障的并發(fā)標(biāo)記和清除。
GC 是非分代的,非緊湊的。
Allocation 是按照大小隔離每個(gè) P 分配的區(qū)域來(lái)完成的,以在消除常見(jiàn)情況下的鎖的同時(shí),最小化碎片。
了解 GC 的好地方,可以從 Richard Jones 的 gchandbook.org 開(kāi)始。
1. GC 執(zhí)行清除終止
? a. Stop the world ,這將導(dǎo)致所有 P 達(dá)到 GC 安全點(diǎn)。
? b. 清除任何未清除過(guò)的 spans ,只有在預(yù)期時(shí)間之前強(qiáng)制執(zhí)行此 GC 周期時(shí),才會(huì)有未清除的 span 。
2. GC 執(zhí)行標(biāo)記階段
? a.?? 準(zhǔn)備標(biāo)記階段,將 gcphase 設(shè)置為 _GCmark (從 _GCoff 開(kāi)始),啟用寫(xiě)屏障,啟用 mutator assist ,并對(duì)根標(biāo)記作業(yè)進(jìn)行排隊(duì)。
在所有 P 都啟用寫(xiě)屏障之前,不會(huì)掃描任何對(duì)象,這是使用 STW 完成的。
? ?b. Start the world ,從現(xiàn)在開(kāi)始,GC 工作由調(diào)度器啟動(dòng)的 標(biāo)記worker 和作 為 allocation 的一部分執(zhí)行的 assists 來(lái)完成。
寫(xiě)屏障將覆寫(xiě)的指針和任何指針寫(xiě)的新指針值都著色。
新分配的對(duì)象立即被標(biāo)記為黑色。
? c.?? GC 執(zhí)行根標(biāo)記作業(yè)。包括: 掃描所有棧 , 著色所有全局變量 ,以及 著色堆外運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)中的任何堆指針 。
掃描棧會(huì)停止goroutine,對(duì)goroutine棧中找到的任何指針進(jìn)行著色,然后恢復(fù)goroutine。
? ? d.?? GC 耗盡灰色對(duì)象的工作隊(duì)列,將每個(gè) 灰色 對(duì)象掃描為 黑色 ,并對(duì)在該對(duì)象中找到的所有指針進(jìn)行著色(反過(guò)來(lái)可能會(huì)將這些指針添加到工作隊(duì)列中)。
? ?e.?? 由于 GC work 分散在本地緩存中,因此 GC 使用 分布式終止算法 來(lái)檢測(cè)何時(shí)不再有根標(biāo)記作業(yè)或灰色對(duì)象(參見(jiàn) gcMarkDone 函數(shù))。
此時(shí),GC 狀態(tài)轉(zhuǎn)換到標(biāo)記終止( gcMarkTermination )。
3. GC 執(zhí)行標(biāo)記終止 gcMarkTermination
? a. Stop the world
? b. 將 gcphase 設(shè)置為 _GCmarktermination ,并禁用 workers 和 assists。
? c. 進(jìn)行內(nèi)務(wù)整理,如 flushing mcaches
4. GC 執(zhí)行清除階段
? ?a. 準(zhǔn)備清除階段,將 gcphase 設(shè)置為 _GCoff ,設(shè)置清除狀態(tài)并禁用寫(xiě)屏障。
? b. Start the world ,從現(xiàn)在開(kāi)始,新分配的對(duì)象是白色的,如有必要,在使用 spans 前 allocating 清除 spans 。
? ?c. GC 在后臺(tái)進(jìn)行 并發(fā)清除 并響應(yīng) allocation ,見(jiàn)下面的描述。
5. 當(dāng)分配足夠時(shí),重復(fù)上面 1 開(kāi)始的步驟,參見(jiàn)下面關(guān)于 GC rate 的討論。
清除階段與正常程序執(zhí)行并發(fā)進(jìn)行。
在后臺(tái) goroutine 中,堆被惰性(當(dāng) goroutine 需要另一個(gè) span 時(shí))且并發(fā)地逐個(gè) span 掃描(這有助于不是 CPU bound 的程序)。
在 STW 標(biāo)記終止 的結(jié)尾,所有的 span 都被標(biāo)記為 需要清除 。
后臺(tái)清除器 goroutine 簡(jiǎn)單地逐個(gè)清除 span 。
為了避免在存在未清除的 span 時(shí)請(qǐng)求更多的 OS內(nèi)存 ,當(dāng) goroutine 需要另一個(gè) span 時(shí),它首先嘗試通過(guò)清除來(lái)回收這些內(nèi)存。
當(dāng) goroutine 需要分配一個(gè)新的 小對(duì)象span 時(shí),它會(huì)清除相同大小的小對(duì)象 span ,直到釋放至少一個(gè)對(duì)象為止。
當(dāng) goroutine 需要從堆中分配 大對(duì)象span 時(shí),它會(huì)清除 span ,直到將至少那么多頁(yè)面釋放到堆中。
有一種情況,這可能是不夠的:如果 goroutine 清除并釋放兩個(gè)不相鄰的 單頁(yè)span 到堆中,那么它將分配一個(gè)新的 雙頁(yè)span ,但是仍然可以有其他 單頁(yè)未清除的span ,可以組合成 雙頁(yè)的span 。
確保在未清除的 span 上不進(jìn)行任何操作(這會(huì)破壞 GC 位圖中的標(biāo)記位)至關(guān)重要。
在 GC 期間,所有 mcache 都被刷新到 中央緩存 中,因此它們是空的。
當(dāng)一個(gè) goroutine 抓取一個(gè)新的 span 到 mcache 時(shí), goroutine 會(huì)清除 mcache 。
當(dāng) goroutine 顯式釋放對(duì)象或設(shè)置 finalizer 時(shí),goroutine 確保 span 已經(jīng)清除(通過(guò)清除或者等待并發(fā)清除完成)。
finalizer goroutine 僅在所有 span 已經(jīng)清除時(shí)才開(kāi)始。
當(dāng)下一次 GC 啟動(dòng)時(shí),它將清除所有尚未清除的 span (如果有的話)。
下一次 GC 是在我們分配了與已經(jīng)使用的內(nèi)存成正比的額外內(nèi)存量之后。
該比例由 GOGC 環(huán)境變量控制(默認(rèn)為 100 )。
如果 GOGC=100 ,而我們使用的是 4M ,那么當(dāng)達(dá)到 8M 時(shí),我們將再次進(jìn)行 GC(此標(biāo)記在 next_gc 變量中被跟蹤)。
獲取 GOGC :
這使得 GC成本 與 allocation 成本 成線性比例。
調(diào)整 GOGC 只會(huì)改變線性常量(以及使用的額外內(nèi)存量)。
為了防止在掃描大型對(duì)象時(shí)出現(xiàn)長(zhǎng)時(shí)間的暫停,并提高并行性,垃圾收集器將大于 maxObletBytes 的對(duì)象的掃描作業(yè)分解為最多 maxObletBytes 的 oblets 。
當(dāng)掃描遇到大對(duì)象時(shí),它只掃描第一個(gè) oblet ,并將其余 oblets 作為新的掃描作業(yè)排隊(duì)。
從Gov1.12版本開(kāi)始,Go使用了非分代的、并發(fā)的、基于三色標(biāo)記清除的垃圾回收器。
關(guān)于垃圾回收,比較常見(jiàn)的算法有引用計(jì)數(shù)、標(biāo)記清除和分代收集,Golang語(yǔ)言使用的垃圾回收算法是標(biāo)記清除。
Golang語(yǔ)言的標(biāo)記清除垃圾回收算法,為了防止GC掃描時(shí)內(nèi)存變化引起的混亂。那么就需要 STW,即Stop The World。具體在Golang語(yǔ)言中是指,在GC時(shí)先停止所有g(shù)oroutine。再進(jìn)行垃圾回收,等待垃圾回收結(jié)束后再恢復(fù)所有被停止的goroutine。
標(biāo)記清除方法
啟動(dòng)STW,暫停程序的業(yè)務(wù)邏輯,找出不可達(dá)對(duì)象和可達(dá)對(duì)象。
將所有可達(dá)對(duì)象做標(biāo)記,清除未標(biāo)記的對(duì)象。停止STW,程序繼續(xù)執(zhí)行。循環(huán)往復(fù),直到進(jìn)程程序生命周期結(jié)束。因?yàn)镾TW需要暫停程序,為了減少暫停程序的時(shí)間。將清除操作移出 STW執(zhí)行周期,但是優(yōu)化效果不明顯。
所謂三色標(biāo)記,實(shí)際上只是為了方便敘述而抽象出來(lái)的一種說(shuō)法,三色對(duì)應(yīng)垃圾回收過(guò)程中對(duì)象的三種狀態(tài)。白色是對(duì)象未被標(biāo)記,gcmarkBits對(duì)應(yīng)位為0,該對(duì)象將會(huì)在本次GC中被清理。灰色是對(duì)象還在標(biāo)記隊(duì)列中等待被標(biāo)記,黑色是對(duì)象已被標(biāo)記,gcmarkBits對(duì)應(yīng)位為0,該對(duì)象將會(huì)在本次 GC中被回收。