GC機制以及Golang的GC機制詳解

要了解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).標記-整理算法

原理:此算法分為標記階段壓縮階段,標記階段和上面標記-清除算法一樣的方式進行對象的標記,但在壓縮整理時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動并整理到一起,并更新對應的指針,具體分為下面三步:

  1. 設定forwarding 指針
  2. 更新指針
  3. 移動對象

標記-整理算法實際上是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是解決了內存碎片的問題,

實際效果如下:

優缺點

  • 可有效利用堆,但是壓縮會有計算成本

?(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)的缺點

  1. STW,stop the world:讓程序暫停,程序出現卡頓?(重要問題)?
  2. 標記需要掃描整個heap(堆)
  3. 清除數據會產生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 階段,有三個問題:

  1. GC 協程和業務協程并行運行的,大概會占用 25% 的CPU,使得程序的吞吐量下降
  2. 如果業務goroutine 分配堆內存太快,導致 Mark(標記) 跟不上Allocate(分配) 的速度,那么業務goroutine會被招募去做協助標記,暫停對業務邏輯的執行,這會影響到服務處理請求的耗時
  3. 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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/717834.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/717834.shtml
英文地址,請注明出處:http://en.pswp.cn/news/717834.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

最長上升子序列(LIS)簡介及其例題分析

一.最長上升子序列(LIS)的相關知識 1.最長上升子序列(Longest Increasing Subsequence),簡稱LIS,也有些情況求的是最長非降序子序列,二者區別就是序列中是否可以有相等的數。假設我們有一個序…

【論文筆記】Initializing Models with Larger Ones

Abstract 介紹權重選擇,一種通過從預訓練模型的較大模型中選擇權重子集來初始化較小模型的方法。這使得知識從預訓練的權重轉移到更小的模型。 它還可以與知識蒸餾一起使用。 權重選擇提供了一種在資源受限的環境中利用預訓練模型力量的新方法,希望能夠…

代碼隨想錄Day67 | 695.島嶼的最大面積 1020.飛地的數量

代碼隨想錄Day67 | 695.島嶼的最大面積 1020.飛地的數量 695.島嶼的最大面積1020.飛地的數量 695.島嶼的最大面積 文檔講解:代碼隨想錄 視頻講解: 狀態 采用bfs,這道題相較于之前的題變為了求島嶼的最大面積。那就說明我們每遇到一個新的島嶼…

【Linux】軟件管理yum | 編輯器vim | vim插件安裝

目錄 1. Linux軟件管理yum 1.1 什么是軟件包 1.2 查看軟件包 1.3 如何安裝軟件 1.4 如何卸載軟件 2. Linux編輯器vim 2.1 vim的基本概念 2.2 vim的基本操作 2.3 vim正常模式命令集 2.4 vim末行模式命令集 2.5 簡單vim配置 2.6 插件安裝 1. Vim-Plug 3. coc.nvim …

如何自己系統的學python

學習Python是一項很好的投資,因為它是一種既強大又易于學習的編程語言,適用于多種應用,如數據分析、人工智能、網站開發等。下面是一個系統學習Python的步驟建議: 基礎準備 安裝Python: 訪問Python官網下載最新版本的…

微服務獲取當前登錄用戶信息

一,實現思路 1,基于JWT令牌登陸方式 JWT實現登錄的,登錄信息就保存在請求頭的token中。因此要獲取當前登錄用戶,只要獲取請求頭,解析其中的token。 1),Gateway網關攔截,解析用戶信…

微信小程序-生命周期

頁面生命周期 onLoad: 頁面加載時觸發的方法,在這個方法中可以進行頁面初始化的操作,如獲取數據、設置頁面狀態等。 onShow: 頁面顯示時觸發的方法,在用戶進入頁面或從其他頁面返回該頁面時會調用此方法。可以在此方法中進行頁面數據刷新、動…

Onenote軟件新建筆記本時報錯:無法在以下位置新建筆記本

報錯現象: 當在OneNote軟件上,新建筆記本時: 然后,嘗試重新登錄微軟賬戶,也不行,提示報錯: 解決辦法: 打開一個新的記事本,復制粘貼以下內容: C:\Users\Adm…

Mysql中的事務

什么是事務: 多條sql語句,要么全部成功,要么全部失敗。 事務的特性: 1:原子性(Atomic): 組成一個事務的多個數據庫操作是一個不可分割的原子單元,只有所有操作都成功,整個事務才會…

在Unity中模擬實現手勢識別功能

在虛擬現實(VR)和增強現實(AR)的應用開發中,手勢識別技術扮演著至關重要的角色,它允許用戶以自然的方式與虛擬世界進行交云。然而,并非所有開發者都有條件使用真實的手勢識別硬件。本文介紹了如何在Unity中通過模擬的方式實現一個簡單的手勢識…

【LeetCode】1768_交替合并字符串_C

題目描述 給你兩個字符串 word1 和 word2 。請你從 word1 開始,通過交替添加字母來合并字符串。如果一個字符串比另一個字符串長,就將多出來的字母追加到合并后字符串的末尾。 返回 合并后的字符串 。 https://leetcode.cn/problems/merge-strings-al…

C++調用lua函數

C 調用Lua全局變量(普通) lua_getglobal(lua, "width");int width lua_tointeger(lua,-1);lua_pop(lua,1);std::cout << width << std::endl;lua_close(lua); 這幾行代碼要放到lua_pcall(lua, 0,0,0);之后才可以. C給lua傳遞變量 lua_pushstring(lua, …

Python 操作 Excel,如何又快又好?

?數據處理是 Python 的一大應用場景&#xff0c;而 Excel 則是最流行的數據處理軟件。因此用 Python 進行數據相關的工作時&#xff0c;難免要和 Excel 打交道。Python處理Excel 常用的系列庫有&#xff1a;xlrd、xlwt、xlutils、openpyxl ?xlrd &#xff0d; 用于讀取 Exce…

點云從入門到精通技術詳解100篇-基于點云網絡和 PSO 優化算法的手勢估計(續)

目錄 3 深度圖像處理及轉化 3.1 雙目深度攝像原理及深度圖的獲取 3.1.1 理想化雙目深度相機成像

day47_servlet

今日內容 0 復習昨日 1 接收請求 2 處理響應 0 復習昨日 HTTP請求中 請求行 請求方法,請求路徑 請求頭 頁面信息 請求正文 請求的數據 HTTP響應中 響應行 狀態碼 信息 響應頭 頁面信息 響應正文 要給瀏覽器的內容 1 接收請求 瀏覽器發出請求,經過web.xml映射匹配,找到Servlet…

STL容器之map和set

map和set ? c98支持的是單參數的隱式類型轉換&#xff0c;而c11支持多參數的隱式類型轉換&#xff1b; 1.map和set的使用 1.1set ? set實現key值不允許修改&#xff0c;是將iterator轉變成const_iterator&#xff1b;可以對同一個類型typedef成兩個不同的自定義標識符。即…

Rocky 9 安裝 R-CytoTRACE

官網給出的詳細指南&#xff0c;只是可能大家打不開或者懶得去看E文。 第一步&#xff0c;下載CytoTRACE安裝包。 wget https://cytotrace.stanford.edu/CytoTRACE_0.3.3.tar.gz 第二步&#xff0c;打開R或者Rstudio-server # 安裝依賴包 if (!requireNamespace("Bioc…

在vue中$nextTick 原理及作用

在vue中$nextTick 原理及作用 Vue 的 nextTick 其本質是對 JavaScript 執行原理 EventLoop 的一種應用。 nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法來模擬對應的微/宏任務的實現&#xff0c;本質是為了利用 Java…

每周AI新聞(2024年第9周)微軟與Mistral AI達成合作 | 谷歌發11B基礎世界模型 | 傳蘋果放棄電動汽車制造轉向生成式AI

這里是陌小北&#xff0c;一個正在研究硅基生命的碳基生命。正在努力成為寫代碼的里面背詩最多的&#xff0c;背詩的里面最會寫段子的&#xff0c;寫段子的里面代碼寫得最好的…廚子。 每周日解讀每周AI大事件。 大廠動向 【1】微軟與Mistral AI達成合作 微軟官宣與法國生成…

視頻云平臺——搭建SRS5平臺支持GB28181視頻流的推送

&#x1f4e2;歡迎點贊 &#xff1a;&#x1f44d; 收藏 ?留言 &#x1f4dd; 如有錯誤敬請指正&#xff0c;賜人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原創&#x1f4e2;作者格言&#xff1a;新的征程&#xff0c;我們面對的不僅…