淺析 Golang 垃圾回收機制

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Google 搜索 "},{"type":"text","marks":[{"type":"strong"}],"text":"Golang GC"},{"type":"text","text":" 排名靠前的文章都講的不錯,從設計到實現,從演進到源碼,一應俱全。但是龐雜的信息會給人一種恐懼感,讓人望而卻步。本文嘗試使用較爲簡單易懂的語言和圖像,講解 Golang 的垃圾回收機制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"垃圾回收算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前比較常見的垃圾回收算法有三種:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"引用計數:爲每個對象維護一個引用計數,當引用該對象的對象銷燬時,引用計數 -1,當對象引用計數爲 0 時回收該對象。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表語言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Python"},{"type":"text","text":"、"},{"type":"text","marks":[{"type":"strong"}],"text":"PHP"},{"type":"text","text":"、"},{"type":"text","marks":[{"type":"strong"}],"text":"Swift"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:對象回收快,不會出現內存耗盡或達到某個閾值時纔回收。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:不能很好的處理循環引用,而實時維護引用計數也是有損耗的。"}]}]}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"標記-清除:從根變量開始遍歷所有引用的對象,標記引用的對象,沒有被標記的進行回收。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表語言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Golang"},{"type":"text","text":"(三色標記法)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:解決了引用計數的缺點。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:需要 STW,暫時停掉程序運行。"}]}]}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"分代收集:按照對象生命週期長短劃分不同的代空間,生命週期長的放入老年代,短的放入新生代,不同代有不同的回收算法和回收頻率。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表語言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Java"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優點:回收性能好"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺點:算法複雜"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Golang 垃圾回收"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跳過原理,我們先來介紹 Golang 的三色標記法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三色標記法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"三色標記法只是爲了敘述方便而抽象出來的一種說法,實際上的對象是沒有三色之分的。這裏的三色,對應了垃圾回收過程中對象的三種狀態:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"灰色:對象還在標記隊列中等待"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"黑色:對象已被標記,"},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 對應位爲 "},{"type":"codeinline","content":[{"type":"text","text":"1"}]},{"type":"text","text":" -- 該對象不會在本次 GC 中被回收"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白色:對象未被標記,"},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 對應位爲 "},{"type":"codeinline","content":[{"type":"text","text":"0"}]},{"type":"text","text":" -- 該對象將會在本次 GC 中被清理"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體流程如下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/46/46a0b312f6e4bb90deea79ace6f945dd.jpeg","alt":null,"title":"三色標記法","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"回收原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上圖,應該對三色標記法有了一個比較直觀的瞭解,那麼我們現在來講講原理。簡單的講,就是標記內存中那些還在使用中(即被引用了)的部分,而內存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要將其回收,以供後續內存分配使用。上圖中的 A、B、D 就是被引用正在使用的內存,而C、F、E 曾經被使用過,但現在沒有任何對象引用,就需要被回收掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 Root 區域主要是程序運行到當前時刻的棧和全局數據區域,是實時正在使用到的內存,當然應該優先標記。而考慮到內存塊中存放的可能是指針,所以還需要遞歸的進行標記,待全部標記完後,就會對未被標記的內存進行回收。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"內存標記"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang 中採用 span 數據結構管理內存,span 中維護了一個個內存塊,並由一個位圖 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 表示內存塊的分配情況,而上文中提到的 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 是記錄每塊內存塊被引用情況的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d1dee05d9d8c2f0ddc777552996d09ce.jpeg","alt":null,"title":"內存標記","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖,"},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 記錄了每塊內存的分配情況,而 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 記錄了每塊內存的標記情況。在標記階段會對每塊內存進行標記,有對象引用的內存標記爲 1,沒有對象引用的爲 0。而 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 的數據結構是完全一樣的,在結束標記後,將 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 指向 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":",則有標記的纔是存活的,這樣就完成了內存回收。而 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 則會在下次標記時重新分配內存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"垃圾回收優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在前文中提到,golang 的垃圾回收算法屬於 "},{"type":"text","marks":[{"type":"strong"}],"text":"標記-清除"},{"type":"text","text":",是需要 STW 的。STW 就是 "},{"type":"text","marks":[{"type":"strong"}],"text":"Stop The World"},{"type":"text","text":" 的意思,在 golang 中就是要停掉所有的 goroutine,專心進行垃圾回收,待垃圾回收結束後再恢復 goroutine。而 STW 時間的長短直接影響了應用的執行,如果時間過長,那將是災難性的。爲了縮短 STW 時間,golang 不對優化垃圾回收算法,其中"},{"type":"text","marks":[{"type":"strong"}],"text":"寫屏障(Write Barrier)"},{"type":"text","text":"和"},{"type":"text","marks":[{"type":"strong"}],"text":"輔助GC(Mutator Assist)"},{"type":"text","text":"就是兩種優化垃圾回收的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"寫屏障(Write Barrier)"},{"type":"text","text":":上面說到的 STW 的目的是防止 GC 掃描時內存變化引起的混亂,而寫屏障就是讓 goroutine 與 GC 同時運行的手段,雖然不能完全消除 STW,但是可以大大減少 STW 的時間。寫屏障在 GC 的特定時間開啓,開啓後指針傳遞時會把指針標記,即本輪不回收,下次 GC 時再確定。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"輔助 GC(Mutator Assist)"},{"type":"text","text":":爲了防止內存分配過快,在 GC 執行過程中,GC 過程中 mutator 線程會併發運行,而 mutator assist 機制會協助 GC 做一部分的工作。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"垃圾回收觸發機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"內存分配量達到閾值"},{"type":"text","text":":每次內存分配都會檢查當前內存分配量是否達到閾值,如果達到閾值則觸發 GC。"},{"type":"codeinline","content":[{"type":"text","text":"閾值 = 上次 GC 內存分配量 * 內存增長率"}]},{"type":"text","text":",內存增長率由環境變量 "},{"type":"codeinline","content":[{"type":"text","text":"GOGC"}]},{"type":"text","text":" 控制,默認爲 100,即每當內存擴大一倍時啓動 GC。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"定時觸發 GC"},{"type":"text","text":":默認情況下,2分鐘觸發一次 GC,該間隔由 "},{"type":"codeinline","content":[{"type":"text","text":"src/runtime/proc.go"}]},{"type":"text","text":" 中的 "},{"type":"codeinline","content":[{"type":"text","text":"forcegcperiod"}]},{"type":"text","text":" 聲明。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"手動觸發 GC"},{"type":"text","text":":在代碼中,可通過使用 "},{"type":"codeinline","content":[{"type":"text","text":"runtime.GC()"}]},{"type":"text","text":" 手動觸發 GC。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC 優化建議"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上文可知,GC 性能是與對象數量有關的,對象越多 GC 性能越差,對程序的影響也越大。所以在開發中要儘量減少對象分配個數,採用對象複用、將小對象組合成大對象或採用小數據類型(如使用 "},{"type":"codeinline","content":[{"type":"text","text":"int8"}]},{"type":"text","text":" 代替 "},{"type":"codeinline","content":[{"type":"text","text":"int"}]},{"type":"text","text":")等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"結語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一門編程語言的垃圾回收機制會直接影響使用其開發應用的性能。在日常開發工作中也因注意到其作用,有助於開發出高性能的應用,這也是 GC 常常在面試中被問到的原因。同時,瞭解 GC 對了解內存管理也很有幫助。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a45c3a602d914f821ca970e51f9dcd2d.gif","alt":null,"title":"歡迎掃描二維碼關注公衆號,瞭解更多雲原生知識","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章