Golang GC

常見垃圾回收機制

引用計數(shù)

對每個對象維護一個引用計數(shù),當引用對象的對象被銷毀時贸伐,引用計數(shù)-1新蟆,如果引用計數(shù) 為0觅赊,則進行垃圾回收
優(yōu)點:對象可以很快的被回收,不會出現(xiàn)內(nèi)存耗盡或達到某個閥值時才回收琼稻。
缺點:不能很好的處理循環(huán)引用吮螺,而且實時維護引用計數(shù),有也一定的代價帕翻。
代表語言:Python鸠补、PHP、Swift

標記-清除

從根變量開始遍歷所有引用的對象熊咽,引用的對象標記為"被引用"莫鸭,沒有被標記的進行回 收。
優(yōu)點:解決了引用計數(shù)的缺點横殴。
缺點:需要STW被因,即要暫時停掉程序運行。
代表語言:Golang(其采用三色標記法)

分代收集

按照對象生命周期長短劃分不同的代空間衫仑,生命周期長的放入老年代梨与,而短的放入新生代,不同代有不能的回收算法和回收頻率文狱。
優(yōu)點:回收性能好
缺點:算法復(fù)雜
代表語言: JAVA

Golang的GC演變

Go V1.3之前的標記-清除(mark and sweep)算法

主要流程:

1粥鞋、暫停業(yè)務(wù)邏輯(啟動STW);
2、開始標記可達對象;
3瞄崇、清除未標記對象;
4呻粹、停止STW,讓程序繼續(xù)跑苏研。
將STW的步驟提前了異步等浊,因為在Sweep清除的時候,可以不需要STW停止摹蘑,因為這些對象已經(jīng)是不可達對象了筹燕,不會出現(xiàn)回收寫沖突等問題。但是無論怎么優(yōu)化,Go V1.3都面臨這個一個重要問題撒踪,就是mark-and-sweep 算法會暫停整個程序过咬。Go V1.3都面臨這個一個重要問題,就是mark-and-sweep 算法會暫停整個程序制妄。

Go V1.5的三色并發(fā)標記法

三色分別指的是:
白色標記表:所在的span的gcmarkBits中對應(yīng)的bit為0
灰色標記表:所在的span的gcmarkBits中對應(yīng)的bit為1掸绞,并且對象在標記隊列中
黑色標記表: 所在的span的gcmarkBits中對應(yīng)的bit為1,并且對象已經(jīng)從標記隊列中取出并處理

三色標記的過程
1.開始標記忍捡,只要是新創(chuàng)建的對象集漾,默認顏色都是標記為"白色",以上圖為例砸脊,當前對象1- 7都是白色
2.從程序(對象根節(jié)點)出發(fā)具篇,即從上圖的程序出發(fā),開始遍歷所有對象凌埂,把遍歷到的對象從白色集合放入到灰色集合驱显,當前對象1,對象4為灰瞳抓,其余為白
3.遍歷灰色集合埃疫,將灰色對象應(yīng)用的對象從白色集合放入灰色集合,之后將原灰色集合放 入黑色集合孩哑,當前1栓霜,4為黑,2横蜒,7為灰胳蛮,3,5丛晌,6為白

  1. 重復(fù)第三步仅炊,直到灰色表中無任何對象
  2. 回收所有白色標記的對象,也就是回收垃圾

沒有STW的三色標記法

基于上述的三色并發(fā)標記法來說, 它是一定要依賴STW的. 因為如果不暫停程序, 程序的邏輯 改變對象引用關(guān)系, 這種動作如果在標記階段做了修改澎蛛,會影響標記結(jié)果的正確性抚垄。來看看 一個場景,如果三色標記法, 標記過程不使用STW將會發(fā)生什么事情?
最后發(fā)現(xiàn)谋逻,本來是對象4合法引用的對象3呆馁,卻被GC給“誤殺”回收掉了。 可以看出毁兆,有兩種情況智哀,在三色標記法中,是不希望被發(fā)生的荧恍。

  1. 條件1: 一個白色對象被黑色對象引用(白色被掛在黑色下)
  2. 條件2: 灰色對象與它之間的可達關(guān)系的白色對象遭到破壞(灰色同時丟了該白色)
    如果當以上兩個條件同時滿足時,就會出現(xiàn)對象丟失現(xiàn)象! 并且,如圖所示的場景中脯厨,如果示例中的白色對象3還有很多下游對象的話, 也會一并都清 理掉鸭津。 為了防止這種現(xiàn)象的發(fā)生黄锤,最簡單的方式就是STW,直接禁止掉其他用戶程序?qū)ο笠?關(guān)系的干擾次氨,但是STW的過程有明顯的資源浪費,對所有的用戶程序都有很大影響摘投。 那么是否可以在保證對象不丟失的情況下合理的盡可能的提高GC效率煮寡,減少STW時間呢? 答案是可以的,我們只要使用一種機制犀呼,嘗試去破壞上面的兩個必要條件就可以了幸撕。

屏障機制

GC的觸發(fā)

memstats.heap_live >= memstats.gc_trigger //heap_live is the number of bytes considered live by the GC
其中memstats.gc_trigger的計算公式是:
trigger = unit64(float64(memstats.heap_marked)*(1+triggerRatio))//heap_marked
is the number of bytes marked by the previous GC tiggerRatio 與goalGrowthRatio正相關(guān) 
goalGrowthRatio = float64(gcpercent)/100

公式中的goalGrowthRatio“目標Heap增長率”通過設(shè)置環(huán)境變量GOGC(gcpercent)調(diào)整,默認值為100外臂。 比如當前程序使用4M堆內(nèi)存坐儿,即memstats.heap_marked內(nèi)存為4M,當程序占用的內(nèi)存 上升到memstats.heap_marked*(1+GOGC/100)=8M時候宋光,gc就會被觸發(fā)貌矿,開始進行相關(guān)的gc操作。
因此罪佳,可以通過GOGC 來做參數(shù)調(diào)優(yōu)逛漫。該參數(shù)主要控制的是下一次 gc開始的時候的內(nèi)存使用量。如何對 GOGC 的參數(shù)進行設(shè)置赘艳,要根據(jù)生產(chǎn)情況中的實際場景來定酌毡,比如 GOGC 參數(shù)提升,來減少 GC 的頻率第练。

主動:默認2min觸發(fā)一次gc阔馋,src/runtime/proc.go:forcegcperiod
被動:runtime.gc()

性能優(yōu)化

1、能返回實例值的函數(shù)就別返回指針娇掏。 go里有獨特的逃逸機制呕寝,指針的返回值一定會發(fā)生變量逃逸,逃逸的行為被理解為從棧中 跑到了堆中婴梧,而堆中的gc是相對較慢的下梢。返回值類型,會在內(nèi)聯(lián)處理后銷毀塞蹭,返回指針類型孽江,會逃逸到堆上,增加gc壓力番电。 這不僅應(yīng)用于函數(shù)返回值情況岗屏,如果有可能的話辆琅,盡量程序中還是要少用指針,因為指針變量在gc的時候會導(dǎo)致二次遍歷这刷,使得整個gc變慢婉烟。
逃逸分析:
一般來說,在程序中全局變量暇屋、內(nèi)存占用大的局部變量似袁、發(fā)生了逃逸的局部變量存在的地方就是堆,這一塊內(nèi)存沒有特定的結(jié)構(gòu)咐刨,也沒有固定的大小昙衅,可以根據(jù)需要進行調(diào)整。 簡單來說有大量數(shù)據(jù)要存的時候定鸟,就存在堆里面而涉。堆是進程級別的。當一個變量需要分配在堆上的時候仔粥,開銷會比較大婴谱,對于go這種帶GC的語言來說,也會增加gc壓力躯泰,同時也容易造成內(nèi)存碎片谭羔。
golang逃逸分析最基本的原則是:如果一個函數(shù)返回的是一個(局部)變量的地址,那么 這個變量就發(fā)生逃逸 麦向。
在golang里面瘟裸,變量分配在何處和是否使用new無關(guān),意味著程序猿無法手動指定某個變 量必須分配在棧上或者堆上诵竭,所以我們需要通過一些方法來確定某個變量到底是分配在了 棧上還是堆上话告。

package main
func main() {
a := f1()
*a++ }

//go:noinline func f1() *int {
i := 1
return &i }

附帶一個逃逸分析的指令: go build -gcflags '-m' main.go 以上逃逸分析結(jié)果:
go build -gcflags '-m' escape.go # command-line-arguments
./escape.go:3:6: can inline main
./escape.go:11:9: &i escapes to heap
./escape.go:10:2: moved to heap: i

能引起變量逃逸到堆上的典型情況

  1. 在方法內(nèi)把局部變量指針返回: 局部變量原本應(yīng)該在棧中分配,在棧中回收卵慰。但是由于 返回時被外部引用沙郭,因此其生命周期大于棧,則溢出裳朋。
  2. 發(fā)送指針或帶有指針的值到 channel 中病线。 在編譯時是沒有辦法知道哪個 goroutine 會在 channel 上接收數(shù)據(jù)。所以編譯器沒法知道變量什么時候才會被釋放鲤嫡。
  3. 在一個切片上存儲指針或帶指針的值送挑。 一個典型的例子就是 []*string 。這會導(dǎo)致切片的內(nèi)容逃逸暖眼。盡管其后面的數(shù)組可能是在棧上分配的惕耕,但其引用的值一定是在堆上。
  4. slice的背后數(shù)組被重新分配了诫肠,因為 append 時可能會超出其容量( cap )司澎。 slice 初始化的地方在編譯時是可以知道的欺缘,它最開始會在棧上分配。如果切片背后的存儲要基于運行時的數(shù)據(jù)進行擴充就會在堆上分配惭缰。
  5. 在 interface 類型上調(diào)用方法浪南。在 interface 類型上調(diào)用方法都是動態(tài)調(diào)度的 —— 方 法的真正實現(xiàn)只能在運行時知道。想像一個 io.Reader 類型的變量 r , 調(diào)用 r.Read(b) 會使得 r 的值和切片b 的背后存儲都逃逸掉漱受,所以會在堆上分配。

逃逸分析

package main import "fmt" 
func main() {
}
ch := make(chan *int, 10) f1(ch)
f2(ch)
func f1(ch chan *int) { a := 1
ch <- &a }
func f2(ch chan *int) { data := <- ch
fmt.Println(data)
}

./escape.go:7:4: &a escapes to heap 驗證2 ./escape.go:8:4: data escapes to heap 驗證5
###對象重用
深度很大的循環(huán)或遞歸中骡送,防止爆棧昂羡。
for i:=1;i<100000;i++{ tmp := t{}
} return

var tmp t
for i:=1;i<100000;i++{
tmp = t{} }
return
在循環(huán)和遞歸里,聲明臨時變量會被存進棧中摔踱,只要不涉及到指針的逃逸虐先,棧上的內(nèi)存在該函數(shù)return后就會釋放。那么如果是已知深度比較淺的循環(huán)(遞歸)內(nèi)部派敷,是不介意出現(xiàn):= 操作的蛹批。
但是在無法預(yù)估for 或者遞歸的深度時,如果深度很大篮愉,循環(huán)了千萬次腐芍,重點推薦第二種方法,雖然函數(shù)return后釋放該函數(shù)在棧上開辟的變量试躏,但是降低了爆棧的風(fēng)險猪勇。 原因很簡單,對于GC開發(fā)者不可能讓一個對象占用100M內(nèi)存跟一萬個對象占用100M 內(nèi)存同樣消耗性能颠蕴,顯然那一個占用100M內(nèi)存的對象泣刹,當發(fā)現(xiàn)它不需要回收的話,我就不需要做什么事情了犀被,而那一萬個對象椅您,我需要逐個檢查是否還有被引用,所以該場景下內(nèi)存大小不是關(guān)鍵寡键,對象數(shù)量才是關(guān)鍵掀泳。

總而言之就是盡量減少對象分配,盡量做到對象的重用昌腰。
對于做到對象重用這一點开伏,為了減少GC,golang提供了對象重用的機制遭商,也就是 sync.Pool對象池固灵。 sync.Pool是可伸縮的,并發(fā)安全的劫流。其大小僅受限于內(nèi)存的大小巫玻,可以被看作是一個存放可重用對象的值的容器丛忆。 設(shè)計的目的是存放已經(jīng)分配的但是暫時不用的對象,在需要用到的時候直接從pool中取仍秤。 任何存放區(qū)其中的值可以在任何時候被刪除而不通知熄诡,在高負載下可以動態(tài)的擴容,在不活躍時對象池會收縮诗力。

合理的選擇數(shù)據(jù)結(jié)構(gòu)

我們知道在有數(shù)據(jù)頻繁插入和刪除的場景下凰浮,slice可以擴容但是要收縮就比較麻煩了,于是想到了鏈表苇本,鏈表要刪除單個節(jié)點的時候袜茧,只需要把節(jié)點從鏈表上斷開,不需要復(fù)制數(shù) 據(jù)瓣窄,效率高于數(shù)組結(jié)構(gòu)笛厦。這里直觀的表示一下兩種數(shù)據(jù)結(jié)構(gòu)的區(qū)別:

type MyData1 struct { 
next *MyData
Id int
Name string }

var mydata1 *MyData1

type MyData2 struct { 
Id int
Name string }

var mydata2 []MyData2

面示例代碼的mydata1用的是鏈表結(jié)構(gòu),每個節(jié)點都有一個指向下一個節(jié)點的指針俺夕,想像下存儲1萬個對象到mydata1裳凸,是不是需要創(chuàng)建1萬個MyData1類型的對象。 示例中的mydata2用的是slice結(jié)構(gòu)劝贸,一個slice就是一個對象姨谷,其中的元素都是這一塊內(nèi)存中的值,而不是對象悬荣,需要注意 []MyData2 和 []*MyData2 是不一樣的菠秒,如果換用第二種 寫法,那么每個元素一樣都是一個對象氯迂,因為這時候slice存的不是值而是指向?qū)ο蟮闹羔樇@些指針每一個都分別指到一個對象。
所以有些情況下嚼蚀,采用 []MyData2這種方式比使用鏈表會大大減少對象數(shù)目禁灼,雖然可能帶來內(nèi)存占用增大,但是會大大降低GC帶來的性能損耗轿曙。

排查和定位GC問題

GODEBUG gctrace =1 go run main.go

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弄捕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子导帝,更是在濱河造成了極大的恐慌守谓,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件您单,死亡現(xiàn)場離奇詭異斋荞,居然都是意外死亡,警方通過查閱死者的電腦和手機虐秦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門平酿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凤优,“玉大人,你說我怎么就攤上這事蜈彼≈妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵幸逆,是天一觀的道長棍辕。 經(jīng)常有香客問我,道長还绘,這世上最難降的妖魔是什么痢毒? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蚕甥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栋荸。我一直安慰自己菇怀,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布晌块。 她就那樣靜靜地躺著爱沟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匆背。 梳的紋絲不亂的頭發(fā)上呼伸,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音钝尸,去河邊找鬼括享。 笑死,一個胖子當著我的面吹牛珍促,可吹牛的內(nèi)容都是我干的铃辖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼猪叙,長吁一口氣:“原來是場噩夢啊……” “哼娇斩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起穴翩,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤犬第,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后芒帕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歉嗓,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年副签,在試婚紗的時候發(fā)現(xiàn)自己被綠了遥椿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片基矮。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冠场,靈堂內(nèi)的尸體忽然破棺而出家浇,到底是詐尸還是另有隱情,我是刑警寧澤碴裙,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布钢悲,位于F島的核電站,受9級特大地震影響舔株,放射性物質(zhì)發(fā)生泄漏莺琳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一载慈、第九天 我趴在偏房一處隱蔽的房頂上張望惭等。 院中可真熱鬧,春花似錦办铡、人聲如沸辞做。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秤茅。三九已至,卻和暖如春童叠,著一層夾襖步出監(jiān)牢的瞬間框喳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工厦坛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留五垮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓粪般,卻偏偏與公主長得像拼余,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亩歹,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 對于學(xué)習(xí)Java的開發(fā)來說, GC并不陌生, 實際上Go的GC流程與Java的CMS實現(xiàn)上不盡相同, 但是流程基本...
    七月天_yif閱讀 16,355評論 4 22
  • 原文http://alblue.cn/articles/2020/07/07/1594131614114.html...
    98k_sw閱讀 5,228評論 0 0
  • golang v1.3前-標記清除(mark and sweep)方法 標記清除再gc前會進行STW匙监,STW - ...
    ericthz閱讀 436評論 0 2
  • 何為GC? GC:Garbage Collection(垃圾回收)垃圾指內(nèi)存中不再使用的內(nèi)存區(qū)域小作,自動發(fā)現(xiàn)與釋放這...
    鬼厲閱讀 1,997評論 0 0
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者亭姥,不喜歡去冒險,但是人生放棄了冒險顾稀,也就放棄了無數(shù)的可能达罗。 ...
    yichen大刀閱讀 6,054評論 0 4