騰訊妹子圖解Golang內(nèi)存分配和垃圾回收
從Go v1.12版本開始,Go使用了非分代的、并發(fā)的、基于三色標記清除的垃圾回收器。相關(guān)標記清除算法可以參考C/C++,而Go是一種靜態(tài)類型的編譯型語言。因此,Go不需要VM,Go應(yīng)用程序二進制文件中嵌入了一個小型運行時(Go runtime),可以處理諸如垃圾收集(GC)、調(diào)度和并發(fā)之類的語言功能。首先讓我們看一下Go內(nèi)部的內(nèi)存管理是什么樣子的。
一、 Golang內(nèi)存管理
這里先簡單介紹一下 Golang 運行調(diào)度。在 Golang 里面有三個基本的概念:G, M, P。
G: Goroutine 執(zhí)行的上下文環(huán)境。
M: 操作系統(tǒng)線程。
P: Processer。進程調(diào)度的關(guān)鍵,調(diào)度器,也可以認為約等于CPU。
一個 Goroutine 的運行需要G+P+M三部分結(jié)合起來。

?圖源:《Golang---內(nèi)存管理(內(nèi)存分配)》
(http://t.zoukankan.com/zpcoding-p-13259943.html)
(一)TCMalloc
Go將內(nèi)存劃分和分組為頁(Page),這和Java的內(nèi)存結(jié)構(gòu)完全不同,沒有分代內(nèi)存,這樣的原因是Go的內(nèi)存分配器采用了TCMalloc的設(shè)計思想:
1.Page
與TCMalloc中的Page相同,x64下1個Page的大小是8KB。上圖的最下方,1個淺藍色的長方形代表1個Page。
2.Span
與TCMalloc中的Span相同,Span是內(nèi)存管理的基本單位,代碼中為mspan,一組連續(xù)的Page組成1個Span,所以上圖一組連續(xù)的淺藍色長方形代表的是一組Page組成的1個Span,另外,1個淡紫色長方形為1個Span。
3.mcache
mcache是提供給P(邏輯處理器)的高速緩存,用于存儲小對象(對象大小<= 32Kb)。盡管這類似于線程堆棧,但它是堆的一部分,用于動態(tài)數(shù)據(jù)。所有類大小的mcache包含scan和noscan類型mspan。Goroutine可以從mcache沒有任何鎖的情況下獲取內(nèi)存,因為一次P只能有一個鎖G。因此,這更有效。mcache從mcentral需要時請求新的span。
4.mcentral
mcentral與TCMalloc中的CentralCache類似,是所有線程共享的緩存,需要加鎖訪問,它按Span class對Span分類,串聯(lián)成鏈表,當mcache的某個級別Span的內(nèi)存被分配光時,它會向mcentral申請1個當前級別的Span。每個mcentral包含兩個mspanList:
empty:雙向span鏈表,包括沒有空閑對象的span或緩存mcache中的span。當此處的span被釋放時,它將被移至non-empty span鏈表。
non-empty:有空閑對象的span雙向鏈表。當從mcentral請求新的span,mcentral將從該鏈表中獲取span并將其移入empty span鏈表。
5.mheap
mheap與TCMalloc中的PageHeap類似,它是堆內(nèi)存的抽象,也是垃圾回收的重點區(qū)域,把從OS申請出的內(nèi)存頁組織成Span,并保存起來。當mcentral的Span不夠用時會向mheap申請,mheap的Span不夠用時會向OS申請,向OS的內(nèi)存申請是按頁來的,然后把申請來的內(nèi)存頁生成Span組織起來,同樣也是需要加鎖訪問的。
6.棧
這是棧存儲區(qū),每個Goroutine(G)有一個棧。在這里存儲了靜態(tài)數(shù)據(jù),包括函數(shù)棧幀,靜態(tài)結(jié)構(gòu),原生類型值和指向動態(tài)結(jié)構(gòu)的指針。這與分配給每個P的mcache不是一回事。
(二)內(nèi)存分配
Go 中的內(nèi)存分類并不像TCMalloc那樣分成小、中、大對象,但是它的小對象里又細分了一個Tiny對象,Tiny對象指大小在1Byte到16Byte之間并且不包含指針的對象。小對象和大對象只用大小劃定,無其他區(qū)分。
核心思想:把內(nèi)存分為多級管理,降低鎖的粒度(只是去mcentral和mheap會申請鎖), 以及多種對象大小類型,減少分配產(chǎn)生的內(nèi)存碎片。
微小對象(Tiny)(size<16B)
使用mcache的微小分配器分配小于16個字節(jié)的對象,并且在單個16字節(jié)塊上可完成多個微小分配。
小對象(尺寸16B?32KB)
大小在16個字節(jié)和32k字節(jié)之間的對象被分配在G運行所在的P的mcache的對應(yīng)的mspan size class上。
大對象(大小>32KB)
大于32 KB的對象直接分配在mheap的相應(yīng)大小類上(size class)。
如果mheap為空或沒有足夠大的頁面滿足分配請求,則它將從操作系統(tǒng)中分配一組新的頁(至少1MB)。
如果對應(yīng)的大小規(guī)格在mcache中沒有可用的塊,則向mcentral申請。
如果mcentral中沒有可用的塊,則向mheap申請,并根據(jù)BestFit 算法找到最合適的mspan。如果申請到的mspan超出申請大小,將會根據(jù)需求進行切分,以返回用戶所需的頁數(shù)。剩余的頁構(gòu)成一個新的mspan放回mheap的空閑列表。
如果mheap中沒有可用span,則向操作系統(tǒng)申請一系列新的頁(最小 1MB)。Go 會在操作系統(tǒng)分配超大的頁(稱作arena)。分配一大批頁會減少和操作系統(tǒng)通信的成本。
(三)內(nèi)存回收
go內(nèi)存會分成堆區(qū)(Heap)和棧區(qū)(Stack)兩個部分,程序在運行期間可以主動從堆區(qū)申請內(nèi)存空間,這些內(nèi)存由內(nèi)存分配器分配并由垃圾收集器負責回收。棧區(qū)的內(nèi)存由編譯器自動進行分配和釋放,棧區(qū)中存儲著函數(shù)的參數(shù)以及局部變量,它們會隨著函數(shù)的創(chuàng)建而創(chuàng)建,函數(shù)的返回而銷毀。如果只申請和分配內(nèi)存,內(nèi)存終將枯竭。Go使用垃圾回收收集不再使用的span,把span釋放交給mheap,mheap對span進行span的合并,把合并后的span加入scav樹中,等待再分配內(nèi)存時,由mheap進行內(nèi)存再分配。因此,Go堆是Go垃圾收集器管理的主要區(qū)域。
二、 標記清除算法
當成功區(qū)分出 Go 垃圾收集器管理區(qū)域的存活對象和死亡對象后,Go 垃圾收集器接下來的任務(wù)就是執(zhí)行GC,釋放無用對象占用的內(nèi)存空間,以便有足夠的可用內(nèi)存空間為新對象分配內(nèi)存。目前常見的垃圾回收算法在上篇《自動的內(nèi)存管理系統(tǒng)實操手冊——Java垃圾回收篇》一文中的“垃圾收集算法”部分已有介紹,而Go使用的是標記清除算法,這是一種非常基礎(chǔ)和常見的垃圾收集算法,于1960年被J.McCarthy等人提出。
當堆空間被耗盡的時,就會STW(也被稱為stop the world),其執(zhí)行過程可以分成標記和清除兩個階段。Go 垃圾收集器從根結(jié)點開始遍歷,執(zhí)行可達性分析算法,遞歸標記所有被引用的對象為存活狀態(tài);標記階段結(jié)束后,垃圾收集器會依次遍歷堆中的對象并清除其中的未被標記為存活的對象。
由于用戶程序在垃圾收集的過程中也不能執(zhí)行(STW)。在可達性分析算法中,Go 的GC Roots一般為全局變量和G Stack中的引用指針,和整堆的對象相比只是極少數(shù),因此它帶來的停頓是非常短暫且相對固定的,不隨堆容量增長。在從GC Roots往下遍歷對象的過程,堆越大,存儲對象越多,遞歸遍歷越復雜,要標記更多對象而產(chǎn)生的停頓時間自然就更長。因此我們需要用到更復雜的機制來解決STW的問題。
三、三色可達性分析
為了解決標記清除算法帶來的STW問題,Go和Java都會實現(xiàn)三色可達性分析標記算法的變種以縮短STW的時間。三色可達性分析標記算法按“是否被訪問過”將程序中的對象分成白色、黑色和灰色:
白色對象 — 對象尚未被垃圾收集器訪問過,在可達性分析剛開始的階段,所有的對象都是白色的,若在分析結(jié)束階段,仍然是白色的對象,即代表不可達。
黑色對象 — 表示對象已經(jīng)被垃圾收集器訪問過,且這個對象的所有引用都已經(jīng)被掃描過,黑色的對象代表已經(jīng)被掃描過而且是安全存活的,如果有其他對象只想黑色對象無需再掃描一遍,黑色對象不可能直接(不經(jīng)過灰色對象)指向某個白色對象。
灰色對象 — 表示對象已經(jīng)被垃圾收集器訪問過,但是這個對象上至少存在一個引用還沒有被掃描過,因為存在指向白色對象的外部指針,垃圾收集器會掃描這些對象的子對象。
三色可達性分析算法大致的流程是(初始狀態(tài)所有對象都是白色):
1.從GC Roots開始枚舉,它們所有的直接引用變?yōu)榛疑ㄒ迫牖疑?/span>),GC Roots變?yōu)楹谏?/span>
2.從灰色集合中取出一個灰色對象進行分析:
將這個對象所有的直接引用變?yōu)榛疑湃牖疑现校?/span>
將這個對象變?yōu)楹谏?/span>
3.重復步驟2,一直重復直到灰色集合為空。
4.分析完成,仍然是白色的對象就是GC Roots不可達的對象,可以作為垃圾被清理。
具體例子如下圖所示,經(jīng)過三色可達性分析,最后白色H為不可達的對象,是需要垃圾回收的對象。

三色標記清除算法本身是不可以并發(fā)或者增量執(zhí)行的,它需要STW,而如果并發(fā)執(zhí)行,用戶程序可能在標記執(zhí)行的過程中修改對象的指針。

這種情況一般會有2種:
1.一種是把原本應(yīng)該垃圾回收的死亡對象錯誤的標記為存活。雖然這不好,但是不會導致嚴重后果,只不過產(chǎn)生了一點逃過本次回收的浮動垃圾而已,下次清理就可以,比如上圖所示的三色標記過程中,用戶程序取消了從B對象到E對象的引用,但是因為B到E已經(jīng)被標記完成不會繼續(xù)執(zhí)行步驟2,所以E對象最終會被錯誤的標記成黑色,不會被回收,這個D就是浮動垃圾,會在下次垃圾收集中清理。
2.一種是把原本存活的對象錯誤的標記為已死亡,導致“對象消失”,這在內(nèi)存管理中是非常嚴重的錯誤。比如上圖所示的三色標記過程中,用戶程序建立了從B對象到H對象的引用(例如B.next =H),接著執(zhí)行D.next=nil,但是因為B到H中不存在灰色對象,因此在這之間不會繼續(xù)執(zhí)行三色并發(fā)標記中的步驟2,D到H之間的鏈接被斷開,所以H對象最終會被標記成白色,會被垃圾收集器錯誤地回收。我們將這種錯誤稱為懸掛指針,即指針沒有指向特定類型的合法對象,影響了內(nèi)存的安全性。

四、屏障技術(shù)
為了解決上述的“對象消失”的現(xiàn)象,Wilson于1994年在理論上證明了,當且僅當以下兩個條件同時滿足時,會產(chǎn)生“對象消失”的問題,即原本應(yīng)該是黑色的對象被誤標為白色:
賦值器插入了一條或多條從黑色對象到白色對象的新引用;
賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。
因此為了我們要解決并發(fā)掃描時的對象消失問題,保證垃圾收集算法的正確性,只需破壞這兩個條件的任意一個即可,屏障技術(shù)就是在并發(fā)或者增量標記過程中保證三色不變性的重要技術(shù)。
內(nèi)存屏障技術(shù)是一種屏障指令,它可以讓CPU或者編譯器在執(zhí)行內(nèi)存相關(guān)操作時遵循特定的約束,目前多數(shù)的現(xiàn)代處理器都會亂序執(zhí)行指令以最大化性能,但是該技術(shù)能夠保證內(nèi)存操作的順序性,在內(nèi)存屏障前執(zhí)行的操作一定會先于內(nèi)存屏障后執(zhí)行的操作。垃圾收集中的屏障技術(shù)更像是一個鉤子方法,它是在用戶程序讀取對象、創(chuàng)建新對象以及更新對象指針時執(zhí)行的一段代碼,根據(jù)操作類型的不同,我們可以將它們分成讀屏障(Read barrier)和寫屏障(Write barrier)兩種,因為讀屏障需要在讀操作中加入代碼片段,對用戶程序的性能影響很大,所以編程語言往往都會采用寫屏障保證三色不變性。
(一)插入寫屏障
Dijkstra在1978年提出了插入寫屏障,也被叫做增量更新,通過如下所示的寫屏障,破壞上述第一個條件(賦值器插入了一條或多條從黑色對象到白色對象的新引用):
func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer)shade(ptr) //先將新下游對象 ptr 標記為灰色*slot = ptr}//說明:添加下游對象(當前下游對象slot, 新下游對象ptr) {//step 1標記灰色(新下游對象ptr)//step 2當前下游對象slot = 新下游對象ptr}//場景:A.添加下游對象(nil, B) //A 之前沒有下游, 新添加一個下游對象B, B被標記為灰色A.添加下游對象(C, B) //A 將下游對象C 更換為B, B被標記為灰色

假設(shè)我們上圖的例子并發(fā)可達性分析中使用插入寫屏障:
1.GC 將根對象Root2指向的B對象標記成黑色并將B對象指向的對象D標記成灰色;
2.用戶程序修改指針,B.next=H這時觸發(fā)寫屏障將H對象標記成灰色;
3.用戶程序修改指針D.next=null;
4.GC依次遍歷程序中的H和D將它們分別標記成黑色。
由于棧上的對象在垃圾回收中被認為是根對象,并沒有寫屏障,那么導致黑色的棧可能指向白色的堆對象,例如上圖1中Root2指向H,且刪除了由D指向H的引用,由于沒有寫屏障,那么H將會被刪除。為了保障內(nèi)存安全,Dijkstra必須為棧上的對象增加寫屏障或者在標記階段完成重新對棧上的對象進行掃描,這兩種方法各有各的缺點,前者會大幅度增加寫入指針的額外開銷,后者重新掃描棧對象時需要暫停程序,垃圾收集算法的設(shè)計者需要在這兩者之前做出權(quán)衡。
(二)刪除寫屏障
Yuasa在1990年的論文Real-time garbage collection on general-purpose machines 中提出了刪除寫屏障,因為一旦該寫屏障開始工作,它會保證開啟寫屏障時堆上所有對象的可達。起始時STW掃描所有的goroutine棧,保證所有堆上在用的對象都處于灰色保護下,所以也被稱作快照垃圾收集(Snapshot GC),這是破壞了“對象消失”的第二個條件(賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用)。
// 黑色賦值器 Yuasa 屏障func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(*slot) 先將*slot標記為灰色*slot = ptr}//說明:添加下游對象(當前下游對象slot, 新下游對象ptr) {//step 1if (當前下游對象slot是灰色 || 當前下游對象slot是白色) {標記灰色(當前下游對象slot) //slot為被刪除對象, 標記為灰色}//step 2當前下游對象slot = 新下游對象ptr}//場景A.添加下游對象(B, nil) //A對象,刪除B對象的引用。B被A刪除,被標記為灰(如果B之前為白)A.添加下游對象(B,?C)?????//A對象,更換下游B變成C。B被A刪除,被標記為灰(如果B之前為白)
上述代碼會在老對象的引用被刪除時,將白色的老對象涂成灰色,這樣刪除寫屏障就可以保證弱三色不變性,老對象引用的下游對象一定可以被灰色對象引用。
但是這樣也會導致一個問題,由于會將有存活可能的對象都標記成灰色,因此最后可能會導致應(yīng)該回收的對象未被回收,這個對象只有在下一個循環(huán)才會被回收,比如下圖的D對象。

由于原始快照的原因,起始也是執(zhí)行STW,刪除寫屏障不適用于棧特別大的場景,棧越大,STW掃描時間越長。
(三)混合寫屏障
在 Go 語言 v1.7版本之前,運行時會使用Dijkstra插入寫屏障保證強三色不變性,但是運行時并沒有在所有的垃圾收集根對象上開啟插入寫屏障。因為應(yīng)用程序可能包含成百上千的Goroutine,而垃圾收集的根對象一般包括全局變量和棧對象,如果運行時需要在幾百個Goroutine的棧上都開啟寫屏障,會帶來巨大的額外開銷,所以 Go 團隊在v1.8結(jié)合上述2種寫屏障構(gòu)成了混合寫屏障,實現(xiàn)上選擇了在標記階段完成時暫停程序、將所有棧對象標記為灰色并重新掃描。
Go 語言在v1.8組合Dijkstra插入寫屏障和Yuasa刪除寫屏障構(gòu)成了如下所示的混合寫屏障,該寫屏障會將被覆蓋的對象標記成灰色并在當前棧沒有掃描時將新對象也標記成灰色:
writePointer(slot, ptr):shade(*slot)if current stack is grey:shade(ptr)*slot = ptr
為了移除棧的重掃描過程,除了引入混合寫屏障之外,在垃圾收集的標記階段,我們還需要將創(chuàng)建的所有新對象都標記成黑色,防止新分配的棧內(nèi)存和堆內(nèi)存中的對象被錯誤地回收,因為棧內(nèi)存在標記階段最終都會變?yōu)楹谏圆辉傩枰匦聮呙钘?臻g。總結(jié)來說主要有這幾點:
GC開始將棧上的對象全部掃描并標記為黑色;
GC期間,任何在棧上創(chuàng)建的新對象,均為黑色;
被刪除的堆對象標記為灰色;
被添加的堆對象標記為灰色。
五、GC演進過程
v1.0 — 完全串行的標記和清除過程,需要暫停整個程序;
v1.1 — 在多核主機并行執(zhí)行垃圾收集的標記和清除階段;
v1.3 — 運行時基于只有指針類型的值包含指針的假設(shè)增加了對棧內(nèi)存的精確掃描支持,實現(xiàn)了真正精確的垃圾收集;將unsafe.Pointer類型轉(zhuǎn)換成整數(shù)類型的值認定為不合法的,可能會造成懸掛指針等嚴重問題;
v1.5 — 實現(xiàn)了基于三色標記清掃的并發(fā)垃圾收集器:
大幅度降低垃圾收集的延遲從幾百 ms 降低至 10ms 以下;
計算垃圾收集啟動的合適時間并通過并發(fā)加速垃圾收集的過程;
v1.6 — 實現(xiàn)了去中心化的垃圾收集協(xié)調(diào)器:
基于顯式的狀態(tài)機使得任意Goroutine都能觸發(fā)垃圾收集的狀態(tài)遷移;
使用密集的位圖替代空閑鏈表表示的堆內(nèi)存,降低清除階段的CPU占用;
v1.7 — 通過并行棧收縮將垃圾收集的時間縮短至2ms以內(nèi);
v1.8 — 使用混合寫屏障將垃圾收集的時間縮短至0.5ms以內(nèi);
v1.9 — 徹底移除暫停程序的重新掃描棧的過程;
v1.10 — 更新了垃圾收集調(diào)頻器(Pacer)的實現(xiàn),分離軟硬堆大小的目標;
v1.12 — 使用新的標記終止算法簡化垃圾收集器的幾個階段;
v1.13 — 通過新的 Scavenger 解決瞬時內(nèi)存占用過高的應(yīng)用程序向操作系統(tǒng)歸還內(nèi)存的問題;
v1.14 — 使用全新的頁分配器優(yōu)化內(nèi)存分配的速度;
v1.15 — 改進編譯器和運行時內(nèi)部的CL 226367,它使編譯器可以將更多的x86寄存器用于垃圾收集器的寫屏障調(diào)用;
v1.16 — Go runtime默認使用MADV_DONTNEED更積極的將不用的內(nèi)存釋放給OS。
六、GC過程
Golang GC 相關(guān)的代碼在runtime/mgc.go文件下,可以看見GC總共分為4個階段(翻譯自Golang v1.16版本源碼):
1.sweep termination(清理終止)
暫停程序,觸發(fā)STW。所有的P(處理器)都會進入safe-point(安全點);
清理未被清理的 span 。如果當前垃圾收集是強制觸發(fā)的,需要處理還未被清理的內(nèi)存管理單元;
2.the mark phase(標記階段)
將GC狀態(tài)gcphase從_GCoff改成_GCmark、開啟寫屏障、啟用協(xié)助線程(mutator assists)、將根對象入隊;
恢復程序執(zhí)行,標記進程(mark workers)和協(xié)助程序會開始并發(fā)標記內(nèi)存中的對象,寫屏障會覆蓋的重寫指針和新指針(標記成灰色),而所有新創(chuàng)建的對象都會被直接標記成黑色;
GC執(zhí)行根節(jié)點的標記,這包括掃描所有的棧、全局對象以及不在堆中的運行時數(shù)據(jù)結(jié)構(gòu)。掃描goroutine棧會導致goroutine停止,并對棧上找到的所有指針加置灰,然后繼續(xù)執(zhí)行g(shù)oroutine;
GC遍歷灰色對象隊列,會將灰色對象變成黑色,并將該指針指向的對象置灰;
由于GC工作分布在本地緩存中,GC會使用分布式終止算法(distributed termination algorithm)來檢測何時不再有根標記作業(yè)或灰色對象,如果沒有了GC會轉(zhuǎn)為mark termination(標記終止)。
3. mark termination(標記終止)
? STW;
將GC狀態(tài)gcphase切換至_GCmarktermination,關(guān)閉gc工作線程和協(xié)助程序;
執(zhí)行housekeeping,例如刷新mcaches。
4. the sweep phase(清理階段)
將GC狀態(tài)gcphase切換至_GCoff來準備清理階段,初始化清理階段并關(guān)閉寫屏障;
恢復用戶程序,從現(xiàn)在開始,所有新創(chuàng)建的對象會標記成白色;如果有必要,在使用前分配清理spans;
后臺并發(fā)清理所有的內(nèi)存管理類單元。
GC過程代碼示例
func gcfinished() *int {p := 1runtime.SetFinalizer(&p, func(_ *int) {println("gc finished")})return &p}func allocate() {_ = make([]byte, int((1<<20)*0.25))}func main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()gcfinished()// 當完成 GC 時停止分配for n := 1; n < 50; n++ {println("#allocate: ", n)allocate()}println("terminate")}
hewittwang@HEWITTWANG-MB0 rtx % GODEBUG=gctrace=1 go run new1.gogc 1 @0.015s 0%: 0.015+0.36+0.043 ms clock, 0.18+0.55/0.64/0.13+0.52 ms cpu, 4->4->0 MB, 5 MB goal, 12 Pgc 2 @0.024s 1%: 0.045+0.19+0.018 ms clock, 0.54+0.37/0.31/0.041+0.22 ms cpu, 4->4->0 MB, 5 MB goal, 12 P....
棧分析
gc 2 : 第一個GC周期: 從程序開始運行到第一次GC時間為0.024 秒: 此次GC過程中CPU 占用率wall clockms clockms : STW,Marking Start, 開啟寫屏障ms : Marking階段ms : STW,Marking終止,關(guān)閉寫屏障CPU timems cpums : STW,Marking Startms : 輔助標記時間ms : 并發(fā)標記時間ms : GC 空閑時間ms : Mark 終止時間MB, 5 MB goal4 MB :標記開始時,堆大小實際值4 MB :標記結(jié)束時,堆大小實際值0 MB :標記結(jié)束時,標記為存活對象大小5 MB :標記結(jié)束時,堆大小預測值12 P ?????:本次GC過程中使用的goroutine 數(shù)量
七、GC觸發(fā)條件
運行時會通過runtime.gcTrigger.test方法決定是否需要觸發(fā)垃圾收集,當滿足觸發(fā)垃圾收集的基本條件(即滿足_GCoff階段的退出條件)時——允許垃圾收集、程序沒有崩潰并且沒有處于垃圾收集循環(huán),該方法會根據(jù)三種不同方式觸發(fā)進行不同的檢查:
//mgc.go 文件 runtime.gcTrigger.testfunc (t gcTrigger) test() bool {//測試是否滿足觸發(fā)垃圾手機的基本條件if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {return false}switch t.kind {case gcTriggerHeap: //堆內(nèi)存的分配達到達控制器計算的觸發(fā)堆大小// Non-atomic access to gcController.heapLive for performance. If// we are going to trigger on this, this thread just// atomically wrote gcController.heapLive anyway and we'll see our// own write.return gcController.heapLive >= gcController.triggercase gcTriggerTime: //如果一定時間內(nèi)沒有觸發(fā),就會觸發(fā)新的循環(huán),該出發(fā)條件由 `runtime.forcegcperiod`變量控制,默認為 2 分鐘;if gcController.gcPercent < 0 {return false}lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))return lastgc != 0 && t.now-lastgc > forcegcperiodcase gcTriggerCycle: //如果當前沒有開啟垃圾收集,則觸發(fā)新的循環(huán);// t.n > work.cycles, but accounting for wraparound.return int32(t.n-work.cycles) > 0}return true}
用于開啟垃圾回收的方法為runtime.gcStart,因此所有調(diào)用該函數(shù)的地方都是觸發(fā)GC的代碼:
runtime.mallocgc申請內(nèi)存時根據(jù)堆大小觸發(fā)GC
runtime.GC用戶程序手動觸發(fā)GC
runtime.forcegchelper后臺運行定時檢查觸發(fā)GC
(一)申請內(nèi)存觸發(fā)runtime.mallocgc
Go運行時會將堆上的對象按大小分成微對象、小對象和大對象三類,這三類對象的創(chuàng)建都可能會觸發(fā)新的GC。
1.當前線程的內(nèi)存管理單元中不存在空閑空間時,創(chuàng)建微對象(noscan &&size
2.當用戶程序申請分配32KB以上的大對象時,一定會構(gòu)建 runtime.gcTrigger結(jié)構(gòu)體嘗試觸發(fā)垃圾收集。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {省略代碼 ...shouldhelpgc := falsedataSize := sizec := getMCache() //嘗試獲取mCache。如果沒啟動或者沒有P,返回nil;省略代碼 ...if size <= maxSmallSize {if noscan && size < maxTinySize { // 微對象分配省略代碼 ...v := nextFreeFast(span)if v == 0 {v, span, shouldhelpgc = c.nextFree(tinySpanClass)}省略代碼 ...} else { //小對象分配省略代碼 ...if v == 0 {v, span, shouldhelpgc = c.nextFree(spc)}省略代碼 ...}} else {shouldhelpgc = true省略代碼 ...}省略代碼 ...if shouldhelpgc { //是否應(yīng)該觸發(fā)gcif t := (gcTrigger{kind: gcTriggerHeap}); t.test() { //如果滿足gc觸發(fā)條件就調(diào)用gcStart()gcStart(t)}}省略代碼 ...return x}
這個時候調(diào)用t.test()執(zhí)行的是gcTriggerHeap情況,只需要判斷gcController.heapLive >= gcController.trigger的真假就可以了。 heapLive表示垃圾收集中存活對象字節(jié)數(shù),trigger表示觸發(fā)標記的堆內(nèi)存大小的;當內(nèi)存中存活的對象字節(jié)數(shù)大于觸發(fā)垃圾收集的堆大小時,新一輪的垃圾收集就會開始。
1.heapLive — 為了減少鎖競爭,運行時只會在中心緩存分配或者釋放內(nèi)存管理單元以及在堆上分配大對象時才會更新;
2.trigger?— 在標記終止階段調(diào)用runtime.gcSetTriggerRatio更新觸發(fā)下一次垃圾收集的堆大小,它能夠決定觸發(fā)垃圾收集的時間以及用戶程序和后臺處理的標記任務(wù)的多少,利用反饋控制的算法根據(jù)堆的增長情況和垃圾收集CPU利用率確定觸發(fā)垃圾收集的時機。
(二)手動觸發(fā)runtime.GC
用戶程序會通過runtime.GC函數(shù)在程序運行期間主動通知運行時執(zhí)行,該方法在調(diào)用時會阻塞調(diào)用方直到當前垃圾收集循環(huán)完成,在垃圾收集期間也可能會通過STW暫停整個程序:
func GC() {//在正式開始垃圾收集前,運行時需要通過runtime.gcWaitOnMark等待上一個循環(huán)的標記終止、標記和清除終止階段完成;n := atomic.Load(&work.cycles)gcWaitOnMark(n)//調(diào)用 `runtime.gcStart` 觸發(fā)新一輪的垃圾收集gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})//`runtime.gcWaitOnMark` 等待該輪垃圾收集的標記終止階段正常結(jié)束;gcWaitOnMark(n + 1)// 持續(xù)調(diào)用 `runtime.sweepone` 清理全部待處理的內(nèi)存管理單元并等待所有的清理工作完成for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {sweep.nbgsweep++Gosched() //等待期間會調(diào)用 `runtime.Gosched` 讓出處理器}//for atomic.Load(&work.cycles) == n+1 && !isSweepDone() {Gosched()}// 完成本輪垃圾收集的清理工作后,通過 `runtime.mProf_PostSweep` 將該階段的堆內(nèi)存狀態(tài)快照發(fā)布出來,我們可以獲取這時的內(nèi)存狀態(tài)mp := acquirem()cycle := atomic.Load(&work.cycles)if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) { //僅限于沒有啟動其他標記終止過程mProf_PostSweep()}releasem(mp)}
(三)后臺運行定時檢查觸發(fā)runtime.forcegchelper
運行時會在應(yīng)用程序啟動時在后臺開啟一個用于強制觸發(fā)垃圾收集的Goroutine,該Goroutine調(diào)用runtime.gcStart嘗試啟動新一輪的垃圾收集:
// start forcegc helper goroutinefunc 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)//該 Goroutine 會在循環(huán)中調(diào)用runtime.goparkunlock主動陷入休眠等待其他 Goroutine 的喚醒goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)if debug.gctrace > 0 {println("GC forced")}// Time-triggered, fully concurrent.gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})}}
參考文獻
1.《Go語言設(shè)計與實現(xiàn)》
(https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/)
2.《一個專家眼中的Go與Java垃圾回收算法大對比》
(https://blog.csdn.net/u011277123/article/details/53991572)
3.《Go語言問題集》
(https://www.bookstack.cn/read/qcrao-Go-Questions/spilt.19.GC-GC.md)
4.《CMS垃圾收集器》
(https://juejin.cn/post/6844903782107578382)
5.《Golang v 1.16版本源碼》
(https://github.com/golang/go)
6.《Golang---內(nèi)存管理(內(nèi)存分配)》
(http://t.zoukankan.com/zpcoding-p-13259943.html)
7.《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》—機械工業(yè)出版社
?作者簡介
汪匯
騰訊后臺開發(fā)工程師
騰訊后臺開發(fā)工程師,負責騰訊看點相關(guān)后端業(yè)務(wù),畢業(yè)于南京大學軟件學院。
推薦閱讀

