要了解Golang的GC機制,就需要了解什么事GC,以及GC有哪幾種實現方式
一.什么是GC
????????當一個電腦上的動態內存不再需要時,就應該予以釋放,以讓出內存,這種內存資源管理,稱為垃圾回收(Garbage Collection),簡稱 GC,垃圾回收(Garbage Collection,簡稱GC)是編程語言中提供的自動的內存管理機制,自動釋放不需要的內存對象,讓出存儲器資源,它在一定程度上解決了內存管理的問題,垃圾(程序不用的內存空間視為垃圾)回收可以有效的防止內存泄露,有效的使用空閑的內存
????????GC過程中無需程序員手動執行,GC機制在現代很多編程語言都支持,GC能力的性能與優劣也是不同語言之間對比度指標之一
二.GC的原理
其實垃圾回收機制的原理就是利用一些算法進行內存的管理,從而有效的防止內存泄漏、有效的利用空閑空間(內存空間)
1.什么是內存泄漏
內存泄露,是從操作系統的角度上來闡述的,形象的比喻就是“操作系統可提供給所有進程的存儲空間(虛擬內存空間)正在被某個進程榨干”,導致的原因就是程序在運行的時候,會不斷地動態開辟的存儲空間,這些存儲空間在在運行結束之后后并沒有被及時釋放掉,應用程序在分配了某段內存之后,由于設計的錯誤,會導致程序失去了對該段內存的控制,而對應的程序又沒有很好的gc機制去對程序申請的空間進行回收,這樣就造成了內存空間的浪費,從而導致內存泄漏
2.怎么找到程序中無用的對象
上面講解了垃圾回收機制的原理就是利用一些算法進行內存的管理,那有哪些算法來進行操作呢,它們是怎樣進行的呢?
任何一種垃圾回收算法一般要做兩件基本事情:
- 找到無用的對象
- 回收將無用對象占用的內存空間,使該空間可被程序再次使用
基本流程如下:
????????找到回收對象-->何時回收-->如何回收-->釋放
那么怎么找到無用的對象呢,有如下兩種方式:
(1).計數法(Reference Counting Collector)
給每個對象添加一個引用計數器,如果被引用則計數器加1,如果引用該對象的對象被銷毀計數器減1,當計數器為0時,代表該對象沒有被引用那就需要回收了
如果兩個對象互相引用怎么辦?比如A引用了B,B又引用了A,那就無法釋放
總結:
- 優點:引用計數收集器可以很快的執,對象可以很快的被回收,不會出現內存耗盡或達到某個閥值時才回收,對程序需要不被長時間打斷的實時環境比較有利
- 缺點:不能檢測出循環引用,而且實時維護引用計數,有也一定的代價,比如:父對象有一個對子對象的引用,子對象反過來引用父對象,這樣,他們的引用計數永遠不可能為0
代表語言:Python、PHP、Swift
(2).根搜索算法
(可達性分析)設立若干種根對象,根對象的子對象也是存活的,當任何一個根對象到某一個對象都無法可達時,那么這個對象就是可回收的
如上圖右側白色部分則為根無法到達,從根變量開始遍歷所有引用的對象,引用的對象標記為"被引用",沒有被標記的會被判斷為垃圾進行回收
在Go語言中,可以當做GC roots的對象有以下幾種:
- 全局變量
- 各個G stack上的變量等
總結:
- 優點:解決了引用計數的缺點
- 缺點:需要STW(Stop The World),STW是gc的最大性能問題,對于gc而言,需要暫時停掉程序運行,也就是暫時停止程序的所有的內存變化,即停止所有的goroutine,等待gc結束之后才恢復
代表語言:Golang(其采用三色標記法)
3.觸發GC的閾值
通過上面的方法找到了要回收的對象,那么在什么時候回收呢,這又是一個需要考慮的問題,這里有幾種Go觸發GC運行的調用方式:
- 閾值(分配內測時調用):默認內存擴大一倍,啟動gc,位置:runtime/malloc.go:mallocgc()
- 定時調用:默認2min觸發一次gc,位置:src/runtime/proc.go:forcegcperiod
- 手動調用:runtime/mgc.go:GC()
了解了觸發GC運用的方式,下面就來看看常見的幾種GC算法
4.GC算法
(1).復制算法
簡單的說就是:把空間里的活動對象復制到其他空間,把原空間里的所有對象都回收掉
復制算法將內存劃分為兩個區間,在任意時間點,所有動態分配的對象都只能分配在其中一個區間(稱為活動區間),而另外一個區間(稱為空閑區間)則是空閑的.當有效內存空間耗盡時,虛擬機將暫停程序運行,開啟復制算法GC線程,接下來GC線程會將活動區間內的存活對象,全部復制到空閑區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址,復制算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,必須要克服50%內存的浪費
具體流程如下:
當From空間被占滿時,GC將活動的對象全部復制到To空間,當復制完成后,該算法會將From空間和To空間互換,GC結束,From 空間和To 空間大小必須一致,這是為了保證能把From 空間中的所有活動對象都收納到To 空間里
優缺點
- 優秀的吞吐量,可實現高速分配,不會發生碎片化
- 但是復制算法需要把堆進行二等分,只有一半的堆能被使用,造成堆的浪費,還有復制算法在復制某個對象時要遞歸復制它子對象,這里會帶來額外的負擔,有棧溢出的可能
(2).標記-清除算法
標記-清除算法采用從根集合進行掃描,對存活的對象標記,標記完畢后,再掃描整個空間中未被標記的對象,進行回收:
標記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由于標記-清除算法直接回收不存活的對象,因此會造成內存碎片,這樣壞處是會產生很多不連續的內存碎片
通過上面知道:標記- 清除算法可以由標記階段和清除階段構成,
標記階段
是把所有活動對象都做上標記的階段,清除階段
是把那些沒有標記的對象,也就是非活動對象回收的階段,通過這兩個階段,就可以令不能利用的內存空間重新得到利用
標記階段
?
在上面標記階段進行標記通常采用的搜索對象算法為:深度優先搜索,深度優先搜索比廣度優先搜索更能壓低內存使用量,因此在標記階段經常用到深度優先搜索,它是一個是縱向搜索,如下圖:
而在進行標記的時候,GC只會收集各個對象的標志位并表格化,不會跟對象一起管理,在標記的時候,不在對象的頭里置位,而是在這個表格中的特定場所置位,像這樣集合了用于標記的位的表格稱為“位圖表格”(bitmap table),利用這個表格進行標記的行為稱為“位圖標記”,位圖表格的實現方法有多種,例如散列表和樹形結構和整數型數組等
清除階段?
????????在清除階段需要將回收的垃圾進行再次利用,這里就需要進行分配操作:在清除階段,把垃圾對象連接到空閑的鏈表,搜索空閑鏈表并尋找大小合適的分塊,然后進行合并操作:在分配的時候有不同的分配策略,根據分配策略的不同可能會產生大量的小分塊,如果它們是連續的,就能把所有的小分塊連在一起形成一個大分塊,這種“連接連續分塊”的操作就叫作合并(coalescing),合并是在清除階段進行的
延遲清除法
清除操作所花費的時間是與堆大小成正比的,如果處理的堆越大,清除算法所花費的時間就越長。
延遲清除法,在標記操作結束后,不一定會進行清除操作,會縮減mutator的暫停時間。
優缺點
- 優點:標記清除算法實現簡單,與其他的的算法組合也就相對簡單,使用了[根搜索算法]找到無用的對象
- 缺點:標記清楚算法不會移動對象,但容易產生碎片化的空間,造成內存浪費,舉個列子,如下:
上圖的「根」指的是「GC root」,通過「根搜索算法」確認是不是垃圾,如果需要3空間的內存,而2空間的內存就存不下,就會被空閑,從而造成內存浪費
代表語言:Golang(其采用三色標記法)?
?(3).標記-整理算法
原理:此算法分為標記階段和壓縮階段,標記階段和上面標記-清除算法一樣的方式進行對象的標記,但在壓縮整理時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動并整理到一起,并更新對應的指針,具體分為下面三步:
- 設定forwarding 指針
- 更新指針
- 移動對象
標記-整理算法實際上是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是解決了內存碎片的問題,
實際效果如下:
優缺點
- 可有效利用堆,但是壓縮會有計算成本
?(4).generation算法(Generational Collector)
原理:不同的對象的生命周期是不一樣的,因此,不同生命周期的對象可以采取不同的回收算法,以便提高回收效.分代收集算法的過程如下:按照對象生命周期長短不同,將堆分為新生代和老年代,生命周期長的放入老年代,而短的放入新生代,根據區域特點選用不同的收集算法,如果新生代朝生夕死,則采用復制算法,老年代采用標記清除,或標記整理
拓展:
①Eden區(80%)和兩塊Survivor區(10%),堆中新生代和老年代占比1:2
②每次使用Eden和一塊Survivor,回收時,將存活的對象一次性復制到另一塊Survivor上,如果另一塊Survivor空間不足,則使用分配擔保機制存入老年代,什么時候從Survivor進入老年代,視垃圾回收器類型而定
優缺點:
- 優點:回收性能好
- 缺點:算法復雜
代表語言: JAVA
三.Go的GC機制詳解
上面列舉了一些GC算法,這里來看看Golang的GC操作
1.演變過程
- Go V1.1: STW
- Go V1.3: 標記-清掃(mark and sweep)法
- Go V1.5: 三色并發標記法
- Go V1.8: 混合寫屏障機制(hybrid write barrier)
go的gc采用了并發標記-清掃( Mark-Sweep)算法的三色標記法,并做了一定改進,大部分的工作是在標記垃圾,基本原理基于[根搜索算法]的根可達性分析,減少了STW的時間
下面就來看看各個階段GoGC的操作
2.Go V1.3以及之前的標記-清除(mark and sweep)算法
這里和前面介紹的算法模塊一樣,此算法主要有兩個主要的步驟:
- 標記(Mark phase)
- 清除(Sweep phase)
具體步驟如下:
- 第一步:暫停程序業務邏輯, 分類出可達和不可達的對象,然后做上標記
圖中表示是程序與對象的可達關系,目前程序的可達對象有對象1-2-3,對象4-7等五個對象
- 第二步:開始標記,程序找出它所有可達的對象,并做上標記:
所以對象1-2-3、對象4-7等五個對象被做上標記
- 第三步:?標記完了之后,然后開始清除未標記的對象:
操作非常簡單,但是有一點需要額外注意:mark and sweep算法在執行的時候,需要程序暫停!即?
STW(stop the world)
,STW的過程中,CPU不執行用戶代碼,全部用于垃圾回收,這個過程的影響很大,所以STW也是一些回收機制最大的難題和希望優化的點,所以在執行第三步的這段時間,程序會暫定停止任何工作,卡在那等待回收執行完畢
- 第四步:?停止暫停,讓程序繼續跑,然后循環重復這個過程,直到process程序生命周期結束
以上就是標記-清除算法的流程
-
標記-清除(mark and sweep)的缺點
- STW,stop the world:讓程序暫停,程序出現卡頓?(重要問題)?
- 標記需要掃描整個heap(堆)
- 清除數據會產生heap(堆)碎片
Go V1.3版本之前就是以上來實施的, 在執行GC的基本流程就是首先啟動STW暫停,然后執行標記,再執行數據回收,最后停止STW,如圖所示:
從上圖來看,全部的GC時間都是包裹在STW范圍之內的,這樣貌似程序暫停的時間過長,影響程序的運行性能,所以Go V1.3 做了簡單的優化,將STW的步驟提前, 減少STW暫停的時間范圍,如下所示:
上圖主要是將STW的步驟提前了一步,因為在Sweep清除的時候,可以不需要STW停止,因為這些對象已經是不可達對象了,不會出現回收寫沖突等問題,這就是上面介紹了的延遲清除算法,但是無論怎么優化,Go V1.3都面臨這個一個重要問題:就是mark-and-sweep 算法會暫停整個程序?
Go是如何面對并這個問題的呢?接下來G V1.5版本 就用三色并發標記法來優化這個問題
3.Go V1.5的三色并發標記法
三色標記法是傳統 Mark-Sweep(標記-清除) 的一個改進,它是一個并發的 GC 算法,GC過程和其他用戶goroutine并發運行,其實大部分的工作還是在標記垃圾,基本原理基于根可達(根搜索算法),但需要一定時間的STW(stop the world)?,所以GC的過程實際上就是通過四個階段的標記來確定清楚的對象都有哪些,具體過程如下:
(1).三種顏色介紹
三色標記法將對象的顏色分為了白、灰、黑,三種顏色
- 白色:該對象沒有被標記過(對象垃圾)
- 灰色:該對象已經被標記過了,但該對象下的屬性沒有全被標記完(GC需要從此對象中去尋找垃圾)
- 黑色:該對象已經被標記過了,且該對象下的屬性也全部都被標記過了(程序所需要的對象)
(2).GC的四個階段
- Mark Prepare - STW: 做標記階段的準備工作,需要停止所有正在運行的goroutine(即STW),標記根對象,啟用內存屏障,內存屏障有點像內存讀寫鉤子,它用于在后續并發標記的過程中,維護三色標記的完備性(三色不變性),這個過程通常很快,大概在10-30微秒
- Marking - Concurrent:標記階段會將大概25%(gcBackgroundUtilization)的P用于標記對象,逐個掃描所有G的堆棧,執行三色標記,在這個過程中,所有新分配的對象都是黑色,被掃描的G會被暫停,掃描完成后恢復,這部分工作叫后臺標記(gcBgMarkWorker),這會降低系統大概25%的吞吐量,比如MAXPROCS=6,那么GC P期望使用率為6*0.25=1.5,這150%P會通過專職(Dedicated)/兼職(Fractional)/懶散(Idle) 三種工作模式的Worker共同來完成。這還沒完,為了保證在Marking過程中,其它G分配堆內存太快,導致Mark跟不上Allocate的速度,還需要其它G配合做一部分標記的工作,這部分工作叫輔助標記(mutator assists),在Marking期間,每次G分配內存都會更新它的”負債指數”(gcAssistBytes),分配得越快,gcAssistBytes越大,這個指數乘以全局的”負載匯率”(assistWorkPerByte),就得到這個G需要幫忙Marking的內存大小(這個計算過程叫revise),也就是它在本次分配的mutator assists工作量(gcAssistAlloc)。
- Mark Termination - STW: 標記階段的最后工作是Mark Termination,關閉內存屏障,停止后臺標記以及輔助標記,做一些清理工作,整個過程也需要STW,大概需要60-90微秒,在此之后,所有的P都能繼續為應用程序G服務了
- Sweeping - Concurrent :在標記工作完成之后,剩下的就是清理過程了,清理過程的本質是將沒有被使用的內存塊整理回收給上一個內存管理層級(mcache -> mcentral -> mheap -> OS),清理回收的開銷被平攤到應用程序的每次內存分配操作中,直到所有內存都Sweeping完成,當然每個層級不會全部將待清理內存都歸還給上一級,避免下次分配再申請的開銷,比如Go1.12對mheap歸還OS內存做了優化,使用NADV_FREE延遲歸還內存
而在Marking - Concurrent 階段,有三個問題:
- GC 協程和業務協程是并行運行的,大概會占用 25% 的CPU,使得程序的吞吐量下降
- 如果業務goroutine 分配堆內存太快,導致 Mark(標記) 跟不上Allocate(分配) 的速度,那么業務goroutine會被招募去做協助標記,暫停對業務邏輯的執行,這會影響到服務處理請求的耗時
- Go GC在穩態場景下可以很好的工作,但是在瞬態場景下,如定時的緩存失效,定時的流量脈沖,GC 影響會急劇上升
在Mark Prepare、Mark Termination - STW 階段,這兩個階段雖然按照官方說法時間會很短,但是在實際的線上服務中,有時會在 trace 圖中觀測到長達十幾 ms 的停頓,原因可能為:OS 線程在做內存申請的時候觸發內存整理被“卡住”,Go Runtime 無法搶占處于這種情況的 goroutine ,進而阻塞 STW 完成
(3).流程說明
通過上面GC的四個階段知道了GC的各個流程,可以通過下面的步驟來進一步說明
- 第一步:每次新創建的對象,默認的顏色都是標記為“白色”,如圖所示:
上圖所示,程序可抵達的內存對象關系如左圖所示,右邊的標記表,是用來記錄目前每個對象的標記顏色分類,這里面需要注意的是:所謂“程序”,則是一些對象的根節點集合,所以如果將“程序”展開,會得到類似如下的表現形式,如圖所示:
- 第二步: 每次GC回收開始, 會從根節點開始遍歷所有對象,把遍歷到的對象從白色集合放入“灰色”集合,如圖所示:
這里 要注意的是:本次遍歷是一次遍歷,非遞歸形式,是從程序抽次可抵達的對象遍歷一層,如上圖所示,當前可抵達的對象是對象1和對象4,那么自然本輪遍歷結束,對象1和對象4就會被標記為灰色,灰色標記表就會多出這兩個對象
- 第三步,:遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合,之后將此灰色對象放入黑色集合,如圖所示:
這一次遍歷是只掃描灰色對象,將灰色對象的第一層遍歷可抵達的對象由白色變為灰色,如:對象2、對象7,而之前的灰色對象1和對象4則會被標記為黑色,同時由灰色標記表移動到黑色標記表中
- 第四步:重復第三步, 直到灰色中無任何對象,如圖所示:
?
當全部的可達對象都遍歷完后,灰色標記表將不再存在灰色對象,目前全部內存的數據只有兩種顏色,黑色和白色,那么黑色對象就是程序邏輯可達(需要的)對象,這些數據是目前支撐程序正常業務運行的,是合法的有用數據,不可刪除,白色的對象是全部不可達對象,目前程序邏輯并不依賴他們,那么白色對象就是內存中目前的垃圾數據,需要被清除
- 第五步: 回收所有的白色標記表的對象, 也就是回收垃圾,如圖所示:
以上將全部的白色對象進行刪除回收,剩下的就是全部依賴的黑色對象
(4).三色標記法所存在問題
三色并發標記法的流程基本上就是上面講解的了
,在三色標記法過程中,這里面可能會有很多并發流程均會被掃描,執行并發流程的內存可能相互依賴,從而引發一些存在性的問題
多標-浮動垃圾問題
看一個流程:
假設 E 已經被標記過了(變成灰色了),此時 D 和 E 斷開了引用,按理來說對象 E/F/G 應該被回收的,但是因為 E 已經變為灰色了,其仍會被當作存活對象繼續遍歷下去,最終的結果是:這部分對象仍會被標記為存活,即本輪 GC 不會回收這部分內存
這部分本應該回收但是沒有回收到的內存,被稱之為“浮動垃圾”
漏標-懸掛指針問題
當 GC 線程已經遍歷到 E 變成灰色,D變成黑色時,灰色 E 斷開引用白色 G ,黑色 D 引用了白色 G,此時切回 GC 線程繼續跑,因為 E 已經沒有對 G 的引用了,所以不會將 G 放到灰色集合,盡管因為 D 重新引用了 G,但因為 D 已經是黑色了,不會再重新做遍歷處理。
最終導致的結果是:G 會一直停留在白色集合中,最后被當作垃圾進行清除。這直接影響到了應用程序的正確性,這也是 Go 需要在 GC 時解決的問題
(4).?屏障機制
為了解決上面的問題,引入屏障技術來保障數據的一致性:為了在GC過程中保證數據的安全,在開始三色標記之前就會加上STW,在掃描確定黑白對象之后再放開STW,但是很明顯這樣的GC掃描的性能是很低的,STW的過程有明顯的資源浪費,對所有的用戶程序都有很大影響,因為整個GC流程會進行兩次STW(Stop The World), 第一次是Mark階段的開始, 第二次是Mark Termination階段,為了解決標記-清除(mark and sweep)算法中的卡頓(stw,stop the world)問題,盡可能的提高GC效率,減少STW時間,這里引入了屏障機制(內存屏障)來解決,它能使CPU或編譯器對在該屏障指令之前和之后發出的內存操作強制執行排序約束,在內存屏障前執行的操作一定會先于內存屏障后執行的操作
- 第一次STW會準備根對象的掃描, 啟動寫屏障(Write Barrier)和輔助GC(mutator assist).
- 第二次STW會重新掃描部分根對象, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist)
而根據操作類型的不同,可以將內存屏障分成 Read barrier(讀屏障)和 Write barrier(寫屏障)兩種,在 Go 中都是使用 Write barrier(寫屏障),原因在《Uniprocessor Garbage Collection Techniques》也提到了:
If a non copying collector is used the use of a read barrier is an unnecessary expense.there is no need to protect the mutator from seeing an invalid version of a pointer. Write barrier techniques are cheaper, because heap writes are several times less common than heap reads對于一個不需要對象拷貝的垃圾回收器來說, Read barrier(讀屏障)代價是很高的,因為對于這類垃圾回收器來說是不需要保存讀操作的版本指針問題。相對來說 Write barrier(寫屏障)代碼更小,因為堆中的寫操作遠遠小于堆中的讀操作。
來下面看看 Write barrier(寫屏障)是如何實現的:
這里要注意的是: 屏障技術是不在棧上應用的,因為要保證棧的運行效率
上面的屏蔽機制是基于一個強-弱三色不變式這個公式來解決的,公式如下:
1).強-弱三色不變式
- 強三色不變式:黑色不能引用白色對象
強三色不變色實際上是強制性的不允許黑色對象引用白色對象,這樣就不會出現有白色對象被誤刪的情況?
- 弱三色不變式:被黑色引用的白色對象都處于灰色保護
弱三色不變式強調,黑色對象可以引用白色對象,但是這個白色對象必須存在其他灰色對象對它的引用,或者可達它的鏈路上游存在灰色對象,這樣實則是黑色對象引用白色對象,白色對象處于一個危險被刪除的狀態,但是上游灰色對象的引用,可以保護該白色對象,使其安全
為了遵循上述的兩個方式,GC算法演進到兩種寫屏障方式,他們“插入屏障”, “刪除屏障”
2).插入屏障
插入屏障只對堆上的內存分配起作用,舉個例子:
在A對象引用B對象的時候,B對象被標記為灰色,(將B掛在A下游,B必須被標記為灰色),遵循三色不變式?(不存在黑色對象引用白色對象的情況了, 因為白色會強制變成灰色),但有一個不足之處:結束時需要STW來重新掃描棧,大約需要10~100ms,下面可以通過幾張流程圖來介紹
?
但是如果棧不添加,當全部三色標記掃描之后,棧上有可能依然存在白色對象被引用的情況(如上圖的對象9). 所以要對棧重新進行三色標記掃描, 但這次為了對象不丟失, 要對本次標記掃描啟動STW暫停. 直到棧空間的三色標記結束.
?
?最后將棧和堆空間 掃描剩余的全部 白色節點清除. 這次STW大約的時間在10~100ms間
3).刪除屏障
刪除屏障適用于棧和堆,在刪除屏障機制下刪除一個節點該節點會被置成灰色,后續會繼續掃描該灰色對象的子對象,該方法就是精準度不夠高,一個對象即使被刪除了最后一個指向它的指針也依舊可以活過這一輪,在下一輪GC中被清理掉
被刪除的對象,如果自身為灰色或者白色,那么被標記為灰色,遵循弱三色不變式?(保護灰色對象到白色對象的路徑不會斷),下面可以通過幾張流程圖來介紹
這種方式的回收精度低,一個對象即使被刪除了最后一個指向它的指針也依舊可以活過這一輪,在下一輪GC中被清理掉
好了,Go V1.5的三色標記法原理和問題基本是就講清楚了,下面講解一下v1.8混合寫屏障機制
4.Go v1.8混合寫屏障機制
(1).原理以及流程
混合寫屏障機制目的是解決上面v1.5屏蔽機制(插入(寫)屏障和刪除(寫)屏障)的短板:
- 插入(寫)屏障:結束時需要STW來重新掃描棧,標記棧上引用的白色對象的存活
- 刪除(寫)屏障:回收精度低,GC開始時STW掃描堆棧來記錄初始快照,這個過程會保護開始時刻的所有存活對象
混合寫屏障的基本思想是:????????
????????正在被覆蓋的對象進行著色,且如果當前棧未掃描完成, 則同樣對指針進行著色,同時,在GC的過程中所有新分配的對象都會立刻變為黑色,在垃圾收集的標記階段,將新建的對象標記成黑色,防止新分配的棧內存和堆內存中的對象被錯誤地回收
Go V1.8版本引入了混合寫屏障機制(hybrid write barrier),避免了對棧re-scan的過程,極大的減少了STW的時間,結合了兩者的優點,具體步驟如下:
- 1.GC開始將棧上的對象全部掃描并標記為黑色(之后不再進行第二次重復掃描,無需STW),
- 2.GC期間,任何在棧上創建的新對象,均為黑色
- 3.被刪除的對象標記為灰色
- 4.被添加的對象標記為灰色
混合寫屏障機制滿足變形的弱三色不變式,可以大幅壓縮第二次STW的時間
這里需要注意:屏障技術是不在棧上應用的,因為要保證棧的運行效率
(2).具體場景分析
對象被一個堆對象刪除引用,成為棧對象的下游
//前提:堆對象4->對象7 = 對象7; ?//對象7 被 對象4引用
棧對象1->對象7 = 堆對象7; ?//將堆對象7 掛在 棧對象1 下游
堆對象4->對象7 = null; ? ?//對象4 刪除引用 對象7
對象被一個棧對象刪除引用,成為另一個棧對象的下游
new 棧對象9;
對象8->對象3 = 對象3; ? ? ?//將棧對象3 掛在 棧對象9 下游
對象2->對象3 = null; ? ? ?//對象2 刪除引用 對象3
延伸一下:?如果對象9引用對象5,棧上沒有屏障,對象5最終還是白色的 這樣不會造成誤刪除嗎? 混合寫屏障是對堆使用的,對棧不使用,如果棧中黑色對象引用一個白色對象,沒有寫屏障,最后白色的要被回收的,如下圖:
對上面的這種情況,是不會出現這種情況的,因為對象9是看不見對象5的,是不可達的,如果對象5是可達對象就不會變成白色了.白色表示已經斷鏈了,是引用不到的,否則在STW遍歷期間,就不會被標記為白色了
再思考一個問題:
????????假如對象2刪掉對對象3的引用,且沒有新的對象重新引用3,對象3在這一輪GC中是否會被回收?
解答:
????????屏障機制不會應用在棧上,那么在這一輪中就不會被回收,要下次掃描才會被標記為白色
?對象被一個堆對象刪除引用,成為另一個堆對象的下游
堆對象10->對象7 = 堆對象7; ? ? ? //將堆對象7 掛在 堆對象10 下游
堆對象4->對象7 = null; ? ? ? ? //對象4 刪除引用 對象7
對象從一個棧對象刪除引用,成為另一個堆對象的下游
堆對象10->對象7 = 堆對象7; ? ? ? //將堆對象7 掛在 堆對象10 下游
堆對象4->對象7 = null; ? ? ? ? //對象4 刪除引用 對象7
Golang中的混合寫屏障滿足
弱三色不變式
,結合了刪除寫屏障和插入寫屏障的優點,只需要在開始時并發掃描各個goroutine的棧,使其變黑并一直保持,這個過程不需要STW,而標記結束后,因為棧在掃描后始終是黑色的,也無需再進行re-scan操作了,減少了STW的時間
5.總結
Go的垃圾回收官方形容為非分代 非緊縮 寫屏障 并發標記清理
非分代是Go GC區別于JVM GC分代模型的特點;
非緊縮意味著在回收垃圾的過程中,不需要像復制算法那樣移動內存中的對象,這樣避免STW過長;標記清除法的字面解釋,就是將可達的內存塊進行標記mark,最后沒有標記的不可達內存塊將進行清理sweep;Golang中實現標記功能的算法就是三色標記法,Golang里面三色標記法會造成錯標問題,使用寫屏障來解決這種問題
- GoV1.3- 普通標記清除法,整體過程需要啟動STW,效率極低
- GoV1.5- 三色標記法, 堆空間啟動寫屏障,棧空間不啟動,全部掃描之后,需要重新掃描一次棧(需要STW),效率普通
- GoV1.8-混合寫屏障機制, 堆空間啟動屏障,棧空間不啟動,整個過程幾乎不需要STW,效率較高
6.GC性能評價標準
- 吞吐量
- 最大暫停時間(需要縮短最大暫停時間)
- 堆使用效率(可用的堆越大,GC 運行越快)
- 訪問的局部性
制作不易,請點贊關注
參考: [譯]Go 垃圾回收指南 | LeonardWang