<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          圖解 Go GC

          共 8599字,需瀏覽 18分鐘

           ·

          2022-04-16 11:14

          轉(zhuǎn)載自曹大公眾號(hào),不光是圖,還有動(dòng)畫(huà),讀完對(duì) Go GC 會(huì)有一個(gè)高層次的理解。




          這一篇是之前給極客時(shí)間 tony bai 老師專欄的供稿,經(jīng)過(guò)編輯的同意,延遲幾個(gè)月后可以在我的個(gè)人號(hào)上發(fā)出~

          本文內(nèi)容只作了解,不建議作為面試題考察。

          武林秘籍救不了段錯(cuò)誤

          包教包會(huì)包分配

          在各種流傳甚廣的 C 語(yǔ)言葵花寶典里,一般都有這么一條神秘的規(guī)則,不能返回局部變量:

          int?*?func(void)?{
          ????int?num?=?1234;
          ????/*?...?*/
          ????return?#
          }

          duang!

          當(dāng)函數(shù)返回后,函數(shù)的棧幀(stack frame)即被銷毀,引用了被銷毀位置的內(nèi)存輕則數(shù)據(jù)錯(cuò)亂,重則 segmentation fault。

          經(jīng)過(guò)了八十一難,終于成為了 C 語(yǔ)言絕世高手,還是逃不過(guò)復(fù)雜的堆上對(duì)象引用關(guān)系導(dǎo)致的 dangling pointer:

          當(dāng) B 被 free 掉之后

          當(dāng) B 被 free 掉之后,應(yīng)用程序依然可能會(huì)使用指向 B 的指針,這是比較典型的 dangling pointer 問(wèn)題,堆上的對(duì)象依賴關(guān)系可能會(huì)非常復(fù)雜。我們要正確地寫(xiě)出 free 邏輯還得先把對(duì)象圖給畫(huà)出來(lái)。

          依賴人去處理這些問(wèn)題是不科學(xué),不合理的。C 和 C++ 程序員已經(jīng)被折磨了數(shù)十年,不應(yīng)該再重蹈覆轍了。

          垃圾回收(Garbage Collection)也被稱為自動(dòng)內(nèi)存管理技術(shù),現(xiàn)代編程語(yǔ)言中使用相當(dāng)廣泛,常見(jiàn)的 Java、Go、C# 均在語(yǔ)言的 runtime 中集成了相應(yīng)的實(shí)現(xiàn)。

          在傳統(tǒng)編程語(yǔ)言中我們需要關(guān)注對(duì)象的分配位置,要自己去選擇對(duì)象分配在堆還是棧上,但在 Go 這門有 GC 的語(yǔ)言中,集成了逃逸分析功能,幫助我們自動(dòng)判斷對(duì)象應(yīng)該在堆上還是棧上,可以使用 `go build -gcflags="-m"` 來(lái)觀察逃逸分析的結(jié)果:

          package?main

          func?main()?{
          ????var?m?=?make([]int,?10240)
          ????println(m[0])
          }

          較大的對(duì)象也會(huì)被放在堆上

          執(zhí)行 gcflags="-m" 的輸出可以看到發(fā)生了逃逸

          若對(duì)象被分配在棧上,其管理成本較低,通過(guò)挪動(dòng)棧頂寄存器就可以實(shí)現(xiàn)對(duì)象的分配和釋放。若分配在堆上,則要經(jīng)歷層層的內(nèi)存申請(qǐng)過(guò)程。但這些流程對(duì)用戶都是透明的,在編寫(xiě)代碼時(shí)我們并不需要在意它。需要優(yōu)化時(shí),才需要研究具體的逃逸分析規(guī)則。

          逃逸分析與垃圾回收結(jié)合在一起,極大地解放了程序員們的心智,我們?cè)诰帉?xiě)代碼時(shí),似乎再也沒(méi)必要去擔(dān)心內(nèi)存的分配和釋放問(wèn)題了。

          然而一切抽象皆有成本,這個(gè)成本要么花在編譯期,要么花在運(yùn)行期。

          GC 這種方案是選擇在運(yùn)行期來(lái)解決問(wèn)題,不過(guò)在極端場(chǎng)景下 GC 本身引起的問(wèn)題依然是令人難以忽視的:

          圖來(lái)自網(wǎng)友,GC 使用了 90% 以上的 CPU 資源

          上圖的場(chǎng)景是在內(nèi)存中緩存了上億的 kv,這時(shí) GC 使用的 CPU 甚至占到了總 CPU 占用的 90% 以上。簡(jiǎn)單粗暴地在內(nèi)存中緩存對(duì)象,到頭來(lái)發(fā)現(xiàn) GC 成為了 CPU 殺手。吃掉了大量的服務(wù)器資源,這顯然不是我們期望的結(jié)果。

          想要正確地分析原因,就需要我們對(duì) GC 本身的實(shí)現(xiàn)機(jī)制有稍微深入一些的理解。

          內(nèi)存管理的三個(gè)參與者

          當(dāng)討論內(nèi)存管理問(wèn)題時(shí),我們主要會(huì)講三個(gè)參與者,mutator,allocator 和 garbage collector。

          • mutator 指的是我們的應(yīng)用,即 application,我們將堆上的對(duì)象看作一個(gè)圖,跳出應(yīng)用來(lái)看的話,應(yīng)用的代碼就是在不停地修改這張堆對(duì)象圖里的指向關(guān)系。下面的圖可以幫我們理解 mutator 對(duì)堆上的對(duì)象的影響:

          應(yīng)用運(yùn)行過(guò)程中會(huì)不斷修改對(duì)象的引用關(guān)系

          • allocator 很好理解,內(nèi)存分配器,應(yīng)用需要內(nèi)存的時(shí)候都要向 allocator 申請(qǐng)。allocator 要維護(hù)好內(nèi)存分配的數(shù)據(jù)結(jié)構(gòu),多線程分配器要考慮高并發(fā)場(chǎng)景下鎖的影響,并針對(duì)性地進(jìn)行設(shè)計(jì)以降低鎖沖突。
          • collector 是垃圾回收器。死掉的堆對(duì)象,不用的堆內(nèi)存要由 collector 回收,最終歸還給操作系統(tǒng)。需要掃描內(nèi)存中存活的堆對(duì)象,掃描完成后,未被掃描到的對(duì)象就是無(wú)法訪問(wèn)的堆上垃圾,需要將其占用內(nèi)存回收掉。

          三者的交互過(guò)程可以用下圖來(lái)表示:

          mutator, allocator 和 collector 的交互過(guò)程

          分配內(nèi)存

          應(yīng)用程序使用 mmap 向 OS 申請(qǐng)內(nèi)存,操作系統(tǒng)提供的接口較為簡(jiǎn)單,mmap 返回的結(jié)果是連續(xù)的內(nèi)存區(qū)域。

          mutator 申請(qǐng)內(nèi)存是以應(yīng)用視角來(lái)看問(wèn)題,我需要的是某一個(gè) struct,某一個(gè) slice 對(duì)應(yīng)的內(nèi)存,這與從操作系統(tǒng)中獲取內(nèi)存的接口之間還有一個(gè)鴻溝。需要由 allocator 進(jìn)行映射與轉(zhuǎn)換,將以“塊”來(lái)看待的內(nèi)存與以“對(duì)象”來(lái)看待的內(nèi)存進(jìn)行映射:

          應(yīng)用代碼中的對(duì)象與內(nèi)存間怎么做映射?

          在現(xiàn)代 CPU 上,我們還要考慮內(nèi)存分配本身的效率問(wèn)題,應(yīng)用執(zhí)行期間小對(duì)象會(huì)不斷地生成與銷毀,如果每一次對(duì)象的分配與釋放都需要與操作系統(tǒng)交互,那么成本是很高的。這需要在應(yīng)用層設(shè)計(jì)好內(nèi)存分配的多級(jí)緩存,盡量減少小對(duì)象高頻創(chuàng)建與銷毀時(shí)的鎖競(jìng)爭(zhēng),這個(gè)問(wèn)題在傳統(tǒng)的 C/C++ 語(yǔ)言中已經(jīng)有了解法,那就是 tcmalloc:

          tcmalloc 的全局圖

          Go 語(yǔ)言的內(nèi)存分配器基本是 tcmalloc 的 1:1 搬運(yùn)。。畢竟都是 Google 的項(xiàng)目。

          在 Go 語(yǔ)言中,根據(jù)對(duì)象中是否有指針以及對(duì)象的大小,將內(nèi)存分配過(guò)程分為三類:

          • tiny :size < 16 bytes && has no pointer(noscan)
          • small :has pointer(scan) || (size >= 16 bytes && size <= 32 KB)
          • large :size > 32 KB

          在內(nèi)存分配過(guò)程中,最復(fù)雜的就是 tiny 類型的分配。

          我們可以將內(nèi)存分配的路徑與 CPU 的多級(jí)緩存作類比,這里 mcache 內(nèi)部的 tiny 可以類比為 L1 cache,而 alloc 數(shù)組中的元素可以類比為 L2 cache,全局的 mheap.mcentral 結(jié)構(gòu)為 L3 cache,mheap.arena 是 L4,L4 是以頁(yè)為單位將內(nèi)存向下派發(fā)的,由 pageAlloc 來(lái)管理 arena 中的空閑內(nèi)存。

          L1L2L3L4
          mcache.tinymcache.alloc[]mheap.centralmheap.arenas

          若 L4 也沒(méi)法滿足我們的內(nèi)存分配需求,便需要向操作系統(tǒng)去要內(nèi)存了。

          和 tiny 的四級(jí)分配路徑相比,small 類型的內(nèi)存沒(méi)有本地的 mcache.tiny 緩存,其余與 tiny 分配路徑完全一致。

          L1L2L3
          mcache.alloc[]mheap.centralmheap.arenas

          large 內(nèi)存分配稍微特殊一些,沒(méi)有上面復(fù)雜的緩存流程,而是直接從 mheap.arenas 中要內(nèi)存,直接走 pageAlloc 頁(yè)分配器。

          頁(yè)分配器在 Go 語(yǔ)言中迭代了多個(gè)版本,從簡(jiǎn)單的 freelist 結(jié)構(gòu),到 treap 結(jié)構(gòu),再到現(xiàn)在最新版本的 radix 結(jié)構(gòu),其查找時(shí)間復(fù)雜度也從 O(N) -> O(log(n)) -> O(1)。

          在當(dāng)前版本中,只需要常數(shù)時(shí)間復(fù)雜度就可以確定空閑頁(yè)組成的 radix tree 是否能夠滿足內(nèi)存分配需求。若不滿足,則要對(duì) arena 繼續(xù)進(jìn)行切分,或向操作系統(tǒng)申請(qǐng)更多的 arena。

          內(nèi)存分配的數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系

          arenas 以 64MB 為單位,arenas 會(huì)被切分成以 8KB 為單位的 page,一個(gè)或多個(gè) page 可以組成一個(gè) mspan,每個(gè) mspan 可以按照 sizeclass 劃分成多個(gè) element。

          如下圖:

          各種內(nèi)存分配結(jié)構(gòu)之間的關(guān)系,圖上省略了頁(yè)分配器的結(jié)構(gòu)

          每一個(gè) mspan 都有一個(gè) allocBits 結(jié)構(gòu),從 mspan 里分配 element 時(shí),只要將 mspan 中對(duì)應(yīng)該 element 位置的 bit 位置一即可。其實(shí)就是將 mspan 對(duì)應(yīng)的 allocBits 中的對(duì)應(yīng) bit 位置一,在代碼中有一些優(yōu)化,我們就不細(xì)說(shuō)了。

          垃圾回收

          Go 語(yǔ)言使用了并發(fā)標(biāo)記與清掃算法作為其 GC 實(shí)現(xiàn),并發(fā)標(biāo)記清掃算法無(wú)法解決內(nèi)存碎片問(wèn)題,而 tcmalloc 恰好一定程度上緩解了內(nèi)存碎片問(wèn)題,兩者配合使用相得益彰。

          這并不是說(shuō) tcmalloc 完全沒(méi)有內(nèi)存碎片,不信你在代碼里搜搜 max waste。

          垃圾分類

          語(yǔ)法垃圾和語(yǔ)義垃圾

          **語(yǔ)義垃圾(semantic garbage)**,有些場(chǎng)景也被稱為內(nèi)存泄露

          語(yǔ)義垃圾指的是從語(yǔ)法上可達(dá)(可以通過(guò)局部、全局變量被引用)的對(duì)象,但從語(yǔ)義上來(lái)講他們是垃圾,垃圾回收器對(duì)此無(wú)能為力。

          我們初始化一個(gè) slice,元素均為指針,每個(gè)指針都指向了堆上 10MB 大小的一個(gè)對(duì)象。

          當(dāng)這個(gè) slice 縮容時(shí),底層數(shù)組的后兩個(gè)元素已經(jīng)無(wú)法再訪問(wèn)了,但其關(guān)聯(lián)的堆上內(nèi)存依然是無(wú)法釋放的。

          碰到類似的場(chǎng)景,你可能需要在縮容前,先將數(shù)組元素置為 nil。

          語(yǔ)法垃圾(syntactic garbage)

          語(yǔ)法垃圾是講那些從語(yǔ)法上無(wú)法到達(dá)的對(duì)象,這些才是垃圾收集器主要的收集目標(biāo)。

          在 allocOnHeap 返回后,堆上的 a 無(wú)法訪問(wèn),便成為了語(yǔ)法垃圾。

          GC 流程

          Go 的每一輪迭代幾乎都會(huì)對(duì) GC 做優(yōu)化。

          經(jīng)過(guò)多次優(yōu)化后,較新的 GC 流程如下圖:

          GC 執(zhí)行流程

          在開(kāi)始并發(fā)標(biāo)記前,并發(fā)標(biāo)記終止時(shí),有兩個(gè)短暫的 stw,該 stw 可以使用 pprof 的 pauseNs 來(lái)觀測(cè),也可以直接采集到監(jiān)控系統(tǒng)中:


          pauseNs 就是每次 stw 的時(shí)長(zhǎng)

          盡管官方聲稱 Go 的 stw 已經(jīng)是亞毫秒級(jí)了,我們?cè)诟邏毫Φ南到y(tǒng)中仍然能夠看到毫秒級(jí)的 stw。

          標(biāo)記流程

          Go 語(yǔ)言使用三色抽象作為其并發(fā)標(biāo)記的實(shí)現(xiàn),首先要理解三種顏色抽象:

          • 黑:已經(jīng)掃描完畢,子節(jié)點(diǎn)掃描完畢。(gcmarkbits = 1,且在隊(duì)列外)
          • 灰:已經(jīng)掃描完畢,子節(jié)點(diǎn)未掃描完畢。(gcmarkbits = 1, 在隊(duì)列內(nèi))
          • 白:未掃描,collector 不知道任何相關(guān)信息。

          三色抽象主要是為了能讓垃圾回收流程與應(yīng)用流程并發(fā)執(zhí)行,這樣將對(duì)象掃描過(guò)程拆分為多個(gè)階段,而不需要一次性完成整個(gè)掃描流程。

          GC 線程與應(yīng)用線程大部分情況下是并發(fā)執(zhí)行的

          GC 掃描的起點(diǎn)是根對(duì)象,忽略掉那些不重要(finalizer 相關(guān)的先省略)的,常見(jiàn)的根對(duì)象可以參見(jiàn)下圖:

          所以在 Go 語(yǔ)言中,從根開(kāi)始掃描的含義是從 .bss 段,.data 段以及 goroutine 的棧開(kāi)始掃描,最終遍歷整個(gè)堆上的對(duì)象樹(shù)。

          標(biāo)記過(guò)程是一個(gè)廣度優(yōu)先的遍歷過(guò)程,掃描節(jié)點(diǎn),將節(jié)點(diǎn)的子節(jié)點(diǎn)推到任務(wù)隊(duì)列中,然后遞歸掃描子節(jié)點(diǎn)的子節(jié)點(diǎn),直到所有工作隊(duì)列都被排空為止。

          后臺(tái)標(biāo)記 worker 的工作過(guò)程

          mark 階段會(huì)將白色對(duì)象標(biāo)記,并推進(jìn)隊(duì)列中變成灰色對(duì)象。我們可以看看 scanobject 的具體過(guò)程:

          在后臺(tái)的 mark worker 執(zhí)行對(duì)象掃描,并將 ptr push 到工作隊(duì)列

          在標(biāo)記過(guò)程中,gc mark worker 會(huì)一邊從工作隊(duì)列(gcw)中彈出對(duì)象,并將其子對(duì)象 push 到工作隊(duì)列(gcw)中,如果工作隊(duì)列滿了,則要將一部分元素向全局隊(duì)列轉(zhuǎn)移。

          我們知道堆上對(duì)象本質(zhì)上是圖,會(huì)存儲(chǔ)引用關(guān)系互相交叉的時(shí)候,在標(biāo)記過(guò)程中也有簡(jiǎn)單的剪枝邏輯:

          如果兩個(gè)后臺(tái) mark worker 分別從 A、B 這兩個(gè)根開(kāi)始標(biāo)記,他們會(huì)重復(fù)標(biāo)記 D 嗎?

          D 是 A 和 B 的共同子節(jié)點(diǎn),在標(biāo)記過(guò)程中自然會(huì)減枝,防止重復(fù)標(biāo)記浪費(fèi)計(jì)算資源:

          標(biāo)記過(guò)程中通過(guò) isMarked 判斷來(lái)進(jìn)行剪枝

          如果多個(gè)后臺(tái) mark worker 確實(shí)產(chǎn)生了并發(fā),標(biāo)記時(shí)使用的是 atomic.Or8,也是并發(fā)安全的。

          標(biāo)記使用 atomic.Or8,是并發(fā)安全的

          協(xié)助標(biāo)記

          當(dāng)應(yīng)用分配內(nèi)存過(guò)快時(shí),后臺(tái)的 mark worker 無(wú)法及時(shí)完成標(biāo)記工作,這時(shí)應(yīng)用本身需要進(jìn)行堆內(nèi)存分配時(shí),會(huì)判斷是否需要適當(dāng)協(xié)助 GC 的標(biāo)記過(guò)程,防止應(yīng)用因?yàn)榉峙溥^(guò)快發(fā)生 OOM。

          碰到這種情況時(shí),我們會(huì)在火焰圖中看到對(duì)應(yīng)的協(xié)助標(biāo)記的調(diào)用棧:

          協(xié)助標(biāo)記會(huì)對(duì)應(yīng)用的響應(yīng)延遲產(chǎn)生影響,可以嘗試降低應(yīng)用的對(duì)象分配數(shù)量進(jìn)行優(yōu)化。

          Go 在內(nèi)部是通過(guò)一套記賬還賬系統(tǒng)來(lái)實(shí)現(xiàn)協(xié)助標(biāo)記的流程的,因?yàn)椴皇潜疚牡闹攸c(diǎn),所以暫且略過(guò)。

          對(duì)象丟失問(wèn)題

          前面提到了 GC 線程/協(xié)程與應(yīng)用線程/協(xié)程是并發(fā)執(zhí)行的,在 GC 標(biāo)記 worker 工作期間,應(yīng)用還會(huì)不斷地修改堆上對(duì)象的引用關(guān)系,下面是一個(gè)典型的應(yīng)用與 GC 同時(shí)執(zhí)行時(shí),由于應(yīng)用對(duì)指針的變更導(dǎo)致對(duì)象漏標(biāo)記,從而被 GC 誤回收的情況。

          如圖所示,在 GC 標(biāo)記過(guò)程中,應(yīng)用動(dòng)態(tài)地修改了 A 和 C 的指針,讓 A 對(duì)象的內(nèi)部指針指向了 B,C 的內(nèi)部指針指向了 D,如果標(biāo)記過(guò)程無(wú)法感知到這種變化,最終 B 對(duì)象在標(biāo)記完成后是白色,會(huì)被錯(cuò)誤地認(rèn)作內(nèi)存垃圾被回收。

          為了解決漏標(biāo),錯(cuò)標(biāo)的問(wèn)題,我們先需要定義“三色不變性”,如果我們的堆上對(duì)象的引用關(guān)系不管怎么修改,都能滿足三色不變性,那么也不會(huì)發(fā)生對(duì)象丟失問(wèn)題。

          強(qiáng)三色不變性(strong tricolor invariant),禁止黑色對(duì)象指向白色對(duì)象。

          強(qiáng)三色不變性

          弱三色不變性(weak tricolor invariant),黑色對(duì)象可以指向白色對(duì)象,但指向的白色對(duì)象,必須有能從灰色對(duì)象可達(dá)的路徑。

          弱三色不變性

          無(wú)論應(yīng)用在與 GC 并發(fā)執(zhí)行期間如何修改堆上對(duì)象的關(guān)系,只要修改之后,堆上對(duì)象能滿足任意一種不變性,就不會(huì)發(fā)生對(duì)象的丟失問(wèn)題。

          而實(shí)現(xiàn)強(qiáng)/弱三色不變性均需要引入屏障技術(shù)。在 Go 語(yǔ)言中,使用寫(xiě)屏障,即 write barrier 來(lái)解決上述問(wèn)題。

          write barrier

          barrier 本質(zhì)是 : snippet of code insert before pointer modify。

          在并發(fā)編程領(lǐng)域也有 memory barrier,但其含義與 GC 領(lǐng)域完全不同,在閱讀相關(guān)材料時(shí),請(qǐng)注意不要混淆。

          Go 語(yǔ)言的 GC 只有 write barrier,沒(méi)有 read barrier。

          在應(yīng)用進(jìn)入 GC 標(biāo)記階段前的 stw 階段,會(huì)將全局變量 runtime.writeBarrier.enabled 修改為 true,這時(shí)所有的堆上指針修改操作在修改之前便會(huì)額外調(diào)用 runtime.gcWriteBarrier:

          在指針修改時(shí)被插入的 write barrier 函數(shù)調(diào)用

          在反匯編結(jié)果中,我們可以通過(guò)行數(shù)找到原始的代碼位置:

          用行數(shù)找到真正的代碼實(shí)現(xiàn)

          常見(jiàn)的 write barrier 有兩種:

          • Dijistra Insertion Barrier,指針修改時(shí),指向的新對(duì)象要標(biāo)灰

          Dijistra 插入屏障

          • Yuasa Deletion Barrier,指針修改時(shí),修改前指向的對(duì)象要標(biāo)灰

          Yuasa 刪除屏障

          Go 的寫(xiě)屏障混合了上述兩種屏障:

          Go 的真實(shí)屏障實(shí)現(xiàn)

          這和 Go 語(yǔ)言在混合屏障的 proposal 上的實(shí)現(xiàn)不太相符,本來(lái) proposal 是這么寫(xiě)的:

          proposal 上聲稱的混合屏障實(shí)現(xiàn)

          但棧的顏色判斷成本是很高的,所以官方還是選擇了更為簡(jiǎn)單的實(shí)現(xiàn),即指針斷開(kāi)的老對(duì)象和新對(duì)象都標(biāo)灰的實(shí)現(xiàn)。

          如果 Go 語(yǔ)言的所有對(duì)象都在堆上分配,理論上我們只要選擇 Dijistra 或者 Yuasa 中的任意一種,就可以實(shí)現(xiàn)強(qiáng)/弱三色不變性了,為什么要做這么復(fù)雜呢?

          因?yàn)樵?Go 語(yǔ)言中,由于棧上對(duì)象操作過(guò)于頻繁,即使在標(biāo)記執(zhí)行階段,棧上對(duì)象也是不開(kāi)啟寫(xiě)屏障的。如果我們只使用 Dijistra 或者只使用 Yuasa Barrier,都會(huì)有對(duì)象丟失的問(wèn)題:

          • Dijistra Insertion Barrier 的對(duì)象丟失問(wèn)題

          棧上的黑色對(duì)象會(huì)指向堆上的白色對(duì)象

          • Yuasa Deletion Barrier 的對(duì)象丟失問(wèn)題

          堆上的黑色對(duì)象會(huì)指向堆上的白色對(duì)象

          早期 Go 只使用了 Dijistra 屏障,但因?yàn)闀?huì)有上述對(duì)象丟失問(wèn)題,需要在第二個(gè) stw 周期進(jìn)行棧重掃(stack rescan)。當(dāng) goroutine 數(shù)量較多時(shí),會(huì)造成 stw 時(shí)間較長(zhǎng)。

          想要消除棧重掃,但單獨(dú)使用任意一種 barrier 都沒(méi)法滿足 Go 的要求,所以最新版本中 Go 的混合屏障其實(shí)是 Dijistra Insertion Barrier ?+ Yuasa Deletion Barrier。

          混合 barrier 的實(shí)現(xiàn)

          混合 write barrier 會(huì)將兩個(gè)指針推到 p 的 wbBuf 結(jié)構(gòu)去,我們來(lái)看看這個(gè)過(guò)程:

          混合 barrier 會(huì)將指針推進(jìn) P 的 wbBuf 結(jié)構(gòu)中,滿了就往 gcw 推

          現(xiàn)在我們可以看看 mutator 和后臺(tái)的 mark worker 在并發(fā)執(zhí)行時(shí)的完整過(guò)程了:

          mutator 和 mark worker 同時(shí)在執(zhí)行時(shí)

          回收流程

          相比復(fù)雜的標(biāo)記流程,對(duì)象的回收和內(nèi)存釋放就簡(jiǎn)單多了。

          進(jìn)程啟動(dòng)時(shí)會(huì)有兩個(gè)特殊 goroutine:

          • 一個(gè)叫 sweep.g,主要負(fù)責(zé)清掃死對(duì)象,合并相關(guān)的空閑頁(yè)
          • 一個(gè)叫 scvg.g,主要負(fù)責(zé)向操作系統(tǒng)歸還內(nèi)存
          (dlv)?goroutines
          *?Goroutine?1?-?User:?./int.go:22?main.main?(0x10572a6)?(thread?5247606)
          ??Goroutine?2?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x102e596)?[force?gc?(idle)?455634h24m29.787802783s]
          ??Goroutine?3?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x102e596)?[GC?sweep?wait]
          ??Goroutine?4?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x102e596)?[GC?scavenge?wait]

          注意看這里的 GC sweep wait 和 GC scavenge wait, 就是這兩個(gè) goroutine

          當(dāng) GC 的標(biāo)記流程結(jié)束之后,sweep goroutine 就會(huì)被喚醒,進(jìn)行清掃工作,其實(shí)就是循環(huán)執(zhí)行 sweepone -> sweep。針對(duì)每個(gè) mspan,sweep.g 的工作是將標(biāo)記期間生成的 bitmap 替換掉分配時(shí)使用的 bitmap:

          mspan:用標(biāo)記期間生成的 bitmap 替換掉分配內(nèi)存時(shí)使用的 bitmap

          然后根據(jù) mspan 中的槽位情況決定該 mspan 的去向:

          • 如果 mspan 中存活對(duì)象數(shù) = 0,即所有 element 都變成了內(nèi)存垃圾,那執(zhí)行 freeSpan -> 歸還組成該 mspan 所使用的頁(yè),并更新全局的頁(yè)分配器摘要信息
          • 如果 mspan 中沒(méi)有空槽,說(shuō)明所有對(duì)象都是存活的,將其放入 fullSwept 隊(duì)列中
          • 如果 mspan 中有空槽,說(shuō)明這個(gè) mspan 還可以拿來(lái)做內(nèi)存分配,將其放入 partialSweep 隊(duì)列中

          之后“清道夫”被喚醒,執(zhí)行線性流程,一路運(yùn)行到將頁(yè)內(nèi)存歸還給操作系統(tǒng):

          • bgscavenge -> pageAlloc.scavenge -> pageAlloc.scavengeOne -> pageAlloc.scavengeRangeLocked -> sysUnused -> madvise

          最終還是要用 madvise 來(lái)將內(nèi)存歸還給操作系統(tǒng)

          問(wèn)題分析

          從前面的基礎(chǔ)知識(shí)中,我們可以總結(jié)出 Go 語(yǔ)言垃圾回收的關(guān)鍵點(diǎn):

          • 無(wú)分代
          • 與應(yīng)用執(zhí)行并發(fā)
          • 協(xié)助標(biāo)記流程
          • 并發(fā)執(zhí)行時(shí)開(kāi)啟 write barrier

          因?yàn)闊o(wú)分代,當(dāng)我們遇到一些需要在內(nèi)存中保留幾千萬(wàn) kv map 的場(chǎng)景(比如機(jī)器學(xué)習(xí)的特征系統(tǒng))時(shí),就需要想辦法降低 GC 掃描成本。

          因?yàn)橛袇f(xié)助標(biāo)記,當(dāng)應(yīng)用的 GC 占用的 CPU 超過(guò) 25% 時(shí),會(huì)觸發(fā)大量的協(xié)助標(biāo)記,影響應(yīng)用的延遲,這時(shí)也要對(duì) GC 進(jìn)行優(yōu)化。

          簡(jiǎn)單的業(yè)務(wù)使用 sync.Pool 就可以帶來(lái)較好的優(yōu)化效果,若碰到一些復(fù)雜的業(yè)務(wù)場(chǎng)景,還要考慮 offheap 之類的欺騙 GC 的方案,比如?dgraph 的方案[1]

          因?yàn)楸酒劢褂趦?nèi)存分配和 GC 的實(shí)現(xiàn),就不展開(kāi)了。

          本文中涉及的所有內(nèi)存管理的名詞,大家都可以在:https://memorymanagement.org 上找到。

          垃圾回收的理論,推薦閱讀:《gc handbook》,可以解答你所有的疑問(wèn)。

          [1]

          dgraph 的方案:?https://dgraph.io/blog/post/manual-memory-management-golang-jemalloc/



          瀏覽 63
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码粗大| 久久亚洲欧美 | 欧美强开小嫩苞 | AⅤ在线视频观看 | 97色噜噜视频 |