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