淺析GO語言的垃圾回收機(jī)制
GO的GC里程碑
v1.3以前:STW
golang的垃圾回收算法都非常簡陋,其性能也廣被詬?。篻o runtime在一定條件下(內(nèi)存超過閾值或定期如2min),暫停所有任務(wù)的執(zhí)行,進(jìn)行mark&sweep操作,操作完成后啟動所有任務(wù)的執(zhí)行。在內(nèi)存使用較多的場景下,go程序在進(jìn)行垃圾回收時會發(fā)生非常明顯的卡頓現(xiàn)象(Stop The World)。在對響應(yīng)速度要求較高的后臺服務(wù)進(jìn)程中,這種延遲簡直是不能忍受的!這個時期國內(nèi)外很多在生產(chǎn)環(huán)境實踐go語言的團(tuán)隊都或多或少踩過gc的坑。當(dāng)時解決這個問題比較常用的方法是盡快控制自動分配內(nèi)存的內(nèi)存數(shù)量以減少gc負(fù)荷,同時采用手動管理內(nèi)存的方法處理需要大量及高頻分配內(nèi)存的場景。
v1.3:Mark STW & Sweep
1.3版本中,go runtime分離了mark和sweep操作,和以前一樣,也是先暫停所有任務(wù)執(zhí)行并啟動mark,mark完成后馬上就重新啟動被暫停的任務(wù)了,而是讓sweep任務(wù)和普通協(xié)程任務(wù)一樣并行的和其他任務(wù)一起執(zhí)行。如果運(yùn)行在多核處理器上,go會試圖將gc任務(wù)放到單獨的核心上運(yùn)行而盡量不影響業(yè)務(wù)代碼的執(zhí)行。go team自己的說法是減少了50%-70%的暫停時間。
v1.5:三色標(biāo)記
go 1.5正在實現(xiàn)的垃圾回收器“非分代的、非移動的、并發(fā)的、三色的標(biāo)記清除垃圾收集器”。這種方法的mark操作是可以漸進(jìn)執(zhí)行的而不需每次都掃描整個內(nèi)存空間,可以減少stop the world的時間。 由此可以看到,一路走來直到1.5版本,go的垃圾回收性能也是一直在提升。
v1.8:混合寫屏障(hybrid write barrier)
由于標(biāo)記操作和用戶邏輯是并發(fā)執(zhí)行的,用戶邏輯會時常生成對象或者改變對象的引用。例如把?個對象標(biāo)記為白色準(zhǔn)備回收時,用戶邏輯突然引用了它,或者又創(chuàng)建了新的對象。由于對象初始時都看為白色,會被 GC 回收掉,為了解決這個問題,引入了寫屏障機(jī)制。
GC 對掃描過后的對象使?操作系統(tǒng)寫屏障功能來監(jiān)控這段內(nèi)存。如果這段內(nèi)存發(fā)?引?改變,寫屏障會給垃圾回收期發(fā)送?個信號,垃圾回收器捕獲到信號后就知道這個對象發(fā)?改變,然后重新掃描這個對象,看看它的引?或者被引?是否改變。利?狀態(tài)的重置實現(xiàn)當(dāng)對象狀態(tài)發(fā)?改變的時候,依然可以再次其引用的對象。
GO的GC
三色標(biāo)記
傳統(tǒng)的標(biāo)記清除算法中,垃圾收集器從垃圾收集的根對象出發(fā),遞歸遍歷這些對象指向的子對象并將所有可達(dá)的對象標(biāo)記成存活;標(biāo)記階段結(jié)束后,垃圾收集器會依次遍歷堆中的對象并清除其中的垃圾,整個過程需要標(biāo)記對象的存活狀態(tài),用戶程序在垃圾收集的過程中也不能執(zhí)行,我們需要用到更復(fù)雜的機(jī)制來解決 STW 的問題,這就出現(xiàn)了三色標(biāo)記法。
三色標(biāo)記算法將程序中的對象分成白色、黑色和灰色三類:
- 白色對象:潛在的垃圾,其內(nèi)存可能會被垃圾收集器回收;
- 黑色對象:活躍的對象,包括不存在任何引用外部指針的對象以及從根對象可達(dá)的對象;
- 灰色對象:活躍的對象,因為存在指向白色對象的外部指針,垃圾收集器會掃描這些對象的子對象;
在垃圾收集器開始工作時,程序中不存在任何的黑色對象,垃圾收集的根對象會被標(biāo)記成灰色,垃圾收集器只會從灰色對象集合中取出對象開始掃描,當(dāng)灰色集合中不存在任何對象時,標(biāo)記階段就會結(jié)束。
三色標(biāo)記垃圾收集器的工作原理很簡單,我們可以將其歸納成以下幾個步驟:
- 從灰色對象的集合中選擇一個灰色對象并將其標(biāo)記成黑色;
- 將黑色對象指向的所有對象都標(biāo)記成灰色,保證該對象和被該對象引用的對象都不會被回收;
- 重復(fù)上述兩個步驟直到對象圖中不存在灰色對象。
當(dāng)三色的標(biāo)記清除的標(biāo)記階段結(jié)束之后,應(yīng)用程序的堆中就不存在任何的灰色對象,我們只能看到黑色的存活對象以及白色的垃圾對象,垃圾收集器可以回收這些白色的垃圾,下面是使用三色標(biāo)記垃圾收集器執(zhí)行標(biāo)記后的堆內(nèi)存,堆中只有對象 D 為待回收的垃圾:
因為用戶程序可能在標(biāo)記執(zhí)行的過程中修改對象的指針,所以三色標(biāo)記清除算法本身是不可以并發(fā)或者增量執(zhí)行的,它仍然需要 STW,在如下所示的三色標(biāo)記過程中,用戶程序建立了從 A 對象到 D 對象的引用,但是因為程序中已經(jīng)不存在灰色對象了,所以 D 對象會被垃圾收集器錯誤地回收。
本來不應(yīng)該被回收的對象卻被回收了,這在內(nèi)存管理中是非常嚴(yán)重的錯誤,我們將這種錯誤稱為懸掛指針,即指針沒有指向特定類型的合法對象,影響了內(nèi)存的安全性,想要并發(fā)或者增量地標(biāo)記對象還是需要使用屏障技術(shù)。
整個流程如下:
混合寫屏障
想要在并發(fā)或者增量的標(biāo)記算法中保證正確性,我們需要達(dá)成以下兩種三色不變性(Tri-color invariant)中的一種:
- 強(qiáng)三色不變性:黑色對象不會指向白色對象,只會指向灰色對象或者黑色對象;
- 弱三色不變性:黑色對象指向的白色對象必須包含一條從灰色對象經(jīng)由多個白色對象的可達(dá)路徑。
上圖分別展示了遵循強(qiáng)三色不變性和弱三色不變性的堆內(nèi)存,遵循上述兩個不變性中的任意一個,我們都能保證垃圾收集算法的正確性,而屏障技術(shù)就是在并發(fā)或者增量標(biāo)記過程中保證三色不變性的重要技術(shù)。
垃圾收集中的屏障技術(shù)更像是一個鉤子方法,它是在用戶程序讀取對象、創(chuàng)建新對象以及更新對象指針時執(zhí)行的一段代碼,根據(jù)操作類型的不同,我們可以將它們分成讀屏障(Read barrier)和寫屏障(Write barrier)兩種,因為讀屏障需要在讀操作中加入代碼片段,對用戶程序的性能影響很大,所以編程語言往往都會采用寫屏障保證三色不變性。
Go 語言在 v1.8 組合 Dijkstra 插入寫屏障和 Yuasa 刪除寫屏障構(gòu)成混合寫屏障,該寫屏障會將被覆蓋的對象標(biāo)記成灰色并在當(dāng)前棧沒有掃描時將新對象也標(biāo)記成灰色。
為了移除棧的重掃描過程,除了引入混合寫屏障之外,在垃圾收集的標(biāo)記階段,我們還需要將創(chuàng)建的所有新對象都標(biāo)記成黑色,防止新分配的棧內(nèi)存和堆內(nèi)存中的對象被錯誤地回收,因為棧內(nèi)存在標(biāo)記階段最終都會變?yōu)楹谏?,所以不再需要重新掃描棧空間。
增量和并發(fā)
傳統(tǒng)的垃圾收集算法會在垃圾收集的執(zhí)行期間暫停應(yīng)用程序,一旦觸發(fā)垃圾收集,垃圾收集器會搶占 CPU 的使用權(quán)占據(jù)大量的計算資源以完成標(biāo)記和清除工作,然而很多追求實時的應(yīng)用程序無法接受長時間的 STW。
為了減少應(yīng)用程序暫停的最長時間和垃圾收集的總暫停時間,我們會使用下面的策略優(yōu)化現(xiàn)代的垃圾收集器:
- 增量垃圾收集:增量地標(biāo)記和清除垃圾,降低應(yīng)用程序暫停的最長時間;
- 并發(fā)垃圾收集:利用多核的計算資源,在用戶程序執(zhí)行時并發(fā)標(biāo)記和清除垃圾;
因為增量和并發(fā)兩種方式都可以與用戶程序交替運(yùn)行,所以我們需要使用屏障技術(shù)保證垃圾收集的正確性;與此同時,應(yīng)用程序也不能等到內(nèi)存溢出時觸發(fā)垃圾收集,因為當(dāng)內(nèi)存不足時,應(yīng)用程序已經(jīng)無法分配內(nèi)存,這與直接暫停程序沒有什么區(qū)別,增量和并發(fā)的垃圾收集需要提前觸發(fā)并在內(nèi)存不足前完成整個循環(huán),避免程序的長時間暫停。
增量收集
增量式(Incremental)的垃圾收集是減少程序最長暫停時間的一種方案,它可以將原本時間較長的暫停時間切分成多個更小的 GC 時間片,雖然從垃圾收集開始到結(jié)束的時間更長了,但是這也減少了應(yīng)用程序暫停的最大時間:
需要注意的是,增量式的垃圾收集需要與三色標(biāo)記法一起使用,為了保證垃圾收集的正確性,我們需要在垃圾收集開始前打開寫屏障,這樣用戶程序修改內(nèi)存都會先經(jīng)過寫屏障的處理,保證了堆內(nèi)存中對象關(guān)系的強(qiáng)三色不變性或者弱三色不變性。雖然增量式的垃圾收集能夠減少最大的程序暫停時間,但是增量式收集也會增加一次 GC 循環(huán)的總時間,在垃圾收集期間,因為寫屏障的影響用戶程序也需要承擔(dān)額外的計算開銷,所以增量式的垃圾收集也不是只帶來好處的,但是總體來說還是利大于弊。
并發(fā)收集
并發(fā)(Concurrent)的垃圾收集不僅能夠減少程序的最長暫停時間,還能減少整個垃圾收集階段的時間,通過開啟讀寫屏障、利用多核優(yōu)勢與用戶程序并行執(zhí)行,并發(fā)垃圾收集器確實能夠減少垃圾收集對應(yīng)用程序的影響:
雖然并發(fā)收集器能夠與用戶程序一起運(yùn)行,但是并不是所有階段都可以與用戶程序一起運(yùn)行,部分階段還是需要暫停用戶程序的,不過與傳統(tǒng)的算法相比,并發(fā)的垃圾收集可以將能夠并發(fā)執(zhí)行的工作盡量并發(fā)執(zhí)行;當(dāng)然,因為讀寫屏障的引入,并發(fā)的垃圾收集器也一定會帶來額外開銷,不僅會增加垃圾收集的總時間,還會影響用戶程序,這是我們在設(shè)計垃圾收集策略時必須要注意的。
GC的時機(jī)
運(yùn)行時會通過如下所示的 runtime.gcTrigger.test 方法決定是否需要觸發(fā)垃圾收集,當(dāng)滿足觸發(fā)垃圾收集的基本條件時 — 允許垃圾收集、程序沒有崩潰并且沒有處于垃圾收集循環(huán),該方法會根據(jù)三種不同方式觸發(fā)進(jìn)行不同的檢查:
func (t gcTrigger) test() bool { if !memstats.enablegc || panicking != 0 || gcphase != _GCoff { return false } switch t.kind { case gcTriggerHeap: return memstats.heap_live >= memstats.gc_trigger case gcTriggerTime: if gcpercent < 0 { return false } lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime)) return lastgc != 0 && t.now-lastgc > forcegcperiod case gcTriggerCycle: return int32(t.n-work.cycles) > 0 } return true }
1、gcTriggerHeap :堆內(nèi)存的分配達(dá)到達(dá)控制器計算的觸發(fā)堆大??;
2、gcTriggerTime :如果一定時間內(nèi)沒有觸發(fā),就會觸發(fā)新的循環(huán),該出發(fā)條件由 runtime.forcegcperiod 變量控制,默認(rèn)為 2 分鐘;
3、gcTriggerCycle:如果當(dāng)前沒有開啟垃圾收集,則觸發(fā)新的循環(huán);
4、runtime.gcpercent 是觸發(fā)垃圾收集的內(nèi)存增長百分比,默認(rèn)情況下為 100,即堆內(nèi)存相比上次垃圾收集增長 100% 時應(yīng)該觸發(fā) GC,并行的垃圾收集器會在到達(dá)該目標(biāo)前完成垃圾收集。
用于開啟垃圾收集的方法 runtime.gcStart 會接收一個 runtime.gcTrigger 類型的結(jié)構(gòu),所有出現(xiàn) runtime.gcTrigger 結(jié)構(gòu)體的位置都是觸發(fā)垃圾收集的代碼:
- runtime.sysmon 和 runtime.forcegchelper :后臺運(yùn)行定時檢查和垃圾收集;
- runtime.GC :用戶程序手動觸發(fā)垃圾收集;
- runtime.mallocgc :申請內(nèi)存時根據(jù)堆大小觸發(fā)垃圾收
以上就是淺析GO語言的垃圾回收機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于GO垃圾回收機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言利用aicli實現(xiàn)輕松調(diào)用DeepSeek和ChatGPT
這篇文章主要為大家介紹了一款用Go語言編寫的AI助手客戶端庫——aicli,該庫不僅支持ChatGPT,還集成了DeepSeek,感興趣的小伙伴可以了解一下2025-03-03簡化Go開發(fā)提高生產(chǎn)力的強(qiáng)大工具及使用詳解
作為?Go?開發(fā)人員,應(yīng)該都知道維持簡潔高效開發(fā)工作流程的重要性,為了提高工作效率和代碼質(zhì)量,簡化開發(fā)流程并自動執(zhí)行重復(fù)性任務(wù)至關(guān)重要,在本文中,我們將探討一些強(qiáng)大的工具和技術(shù),它們將簡化?Go?開發(fā)過程,助力您的編碼之旅2023-10-10Go語言設(shè)計實現(xiàn)在任務(wù)欄里提醒你喝水的兔子
這篇文章主要為大家介紹了Go語言設(shè)計實現(xiàn)在任務(wù)欄里提醒你喝水的兔子示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01基于Golang實現(xiàn)統(tǒng)一加載資源的入口
當(dāng)我們需要在?main?函數(shù)中做一些初始化的工作,比如初始化日志,初始化配置文件,都需要統(tǒng)一初始化入口函數(shù),所以本文就來編寫一個統(tǒng)一加載資源的入口吧2023-05-05