Golang GC 探究

jopen 9年前發布 | 132K 次閱讀 Golang GC Google Go/Golang開發
 

在實際使用go語言的過程中,碰到了一些看似奇怪的內存占用現象,于是決定對go語言的垃圾回收模型進行一些研究。本文對研究的結果進行一下總結。

什么是垃圾回收?

曾幾何時,內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序員必須對內存小心的進行管理操作,控制內存的申請及釋放。稍有不慎,就可能產生 內存泄露 問題,

Golang GC 探究

這種問題不易發現并且難以定位,一直成為困擾開發者的噩夢。如何解決這個頭疼的問題呢?過去一般采用兩種辦法:

  1. 內存泄露檢測工具。這種工具的原理一般是靜態代碼掃描,通過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。
  2. 智能指針。這是c++中引入的自動內存管理方法,通過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存 自動釋放的目的。這種方法是采用最廣泛的做法,但是對程序員有一定的學習成本(并非語言層面的原生支持),而且一旦有忘記使用的場景依然無法避免內存泄 露。

為了解決這個問題,后來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行為就被稱為 垃圾回收

常見的垃圾回收方法

  • 引用計數(reference counting)

這是最簡單的一種垃圾回收算法,和之前提到的智能指針異曲同工。對每個對象維護一個 引用計數 ,當引用該對象的對象被銷毀或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數為0時則立即回收對象。

這種方法的優點是實現簡單,并且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛,如ios cocoa框架,php,python等。簡單引用計數算法也有明顯的缺點:

  1. 頻繁更新引用計數降低了性能。一種簡單的解決方法就是編譯器將相鄰的引用計數更新操作合并到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時通過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有很多其他方法,具體可以參考 這里
  2. 循環引用問題。當對象間發生循環引用時引用鏈中的對象都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用并主動打破循環鏈。當然這也增加了垃圾回收的復雜度。
  • 標記-清除(mark and sweep)

該方法分為兩步, 標記 從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記為“被引用”;標記完成后進行 清除 操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停 當前所有的正常代碼執行,回收是系統響應能力大大降低!當然后續也出現了很多mark&sweep算法的變種(如 三色標記法 )優化了這個問題。

  • 分代收集(generation)

經過大量實際觀察得知,在面向對象編程語言中,絕大多數對象的生命周期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為 代(generation) 的空間。新創建的對象存放在稱為 新生代(young generation) 中(一般來說,新生代的大小會比 老年代 小很多),隨著垃圾回收的重復執行,生命周期較長的對象會被 提升(promotion) 到老年代中。因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用于對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度非常 快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命周期都很短,根本無需提升到老年 代。

GO的垃圾回收器

go語言垃圾回收總體采用的是經典的mark and sweep算法。

  • 1.3版本以前,golang的垃圾回收算法都非常簡陋,然后其性能也廣被詬病:go runtime在一定條件下(內存超過閾值或定期如2min),暫停所有任務的執行,進行mark&sweep操作,操作完成后啟動所有任務的執 行。在內存使用較多的場景下,go程序在進行垃圾回收時會發生非常明顯的卡頓現象(Stop The World)。在對響應速度要求較高的后臺服務進程中,這種延遲簡直是不能忍受的!這個時期國內外很多在生產環境實踐go語言的團隊都或多或少踩過gc的 坑。當時解決這個問題比較常用的方法是盡快控制自動分配內存的內存數量以減少gc負荷,同時采用手動管理內存的方法處理需要大量及高頻分配內存的場景。
  • 1.3版本開始go team開始對gc性能進行持續的改進和優化,每個新版本的go發布時gc改進都成為大家備受關注的要點。1.3版本中,go runtime分離了mark和sweep操作,和以前一樣,也是先暫停所有任務執行并啟動mark,mark完成后馬上就重新啟動被暫停的任務了,而是 讓sweep任務和普通協程任務一樣并行的和其他任務一起執行。如果運行在多核處理器上,go會試圖將gc任務放到單獨的核心上運行而盡量不影響業務代碼 的執行。go team自己的說法是減少了50%-70%的暫停時間。
  • 1.4版本(當前最新穩定版本)對gc的性能改動并不多。1.4版本中runtime很多代碼取代了原生c語言實現而采用了go語言實現,對 gc帶來的一大改變是可以是實現精確的gc。c語言實現在gc時無法獲取到內存的對象信息,因此無法準確區分普通變量和指針,只能將普通變量當做指針,如 果碰巧這個普通變量指向的空間有其他對象,那這個對象就不會被回收。而go語言實現是完全知道對象的類型信息,在標記時只會遍歷指針指向的對象,這樣就避 免了C實現時的堆內存浪費(解決約10-30%)。
  • 1.5版本go team對gc又進行了比較大的改進(1.4中已經埋下伏筆如write barrier的引入),官方的主要目標是減少延遲。go 1.5正在實現的垃圾回收器是“非分代的、非移動的、并發的、三色的標記清除垃圾收集器”。分代算法上文已經提及,是一種比較好的垃圾回收管理策略,然 1.5版本中并未考慮實現;我猜測的原因是步子不能邁太大,得逐步改進,go官方也表示會在1.6版本的gc優化中考慮。同時引入了上文介紹的三色標記 法,這種方法的mark操作是可以漸進執行的而不需每次都掃描整個內存空間,可以減少stop the world的時間。

由此可以看到,一路走來直到1.5版本,go的垃圾回收性能也是一直在提升,但是相對成熟的垃圾回收系統(如java jvm和javascript v8),go需要優化的路徑還很長(但是相信未來一定是美好的~)。

實踐經驗

團隊在實踐go語言時同樣碰到最多和最棘手的問題也是內存問題(其中gc為主),這里把遇到的問題和經驗總結下,歡迎大家一起交流探討。

  1. go程序內存占用大的問題 。這個問題在我們對后臺服務進行壓力測試時發現,我們模擬大量的用戶請求訪問后臺服務,這時各服務模塊能觀察到明顯的內存占用上升。但是當停止壓測時,內 存占用并未發生明顯的下降。花了很長時間定位問題,使用gprof等各種方法,依然沒有發現原因。最后發現原來這時正常的…主要的原因有兩個,一是go的 垃圾回收有個觸發閾值,這個閾值會隨著每次內存使用變大而逐漸增大(如初始閾值是10MB則下一次就是20MB,再下一次就成為了40MB…),如果長時 間沒有觸發gc go會主動觸發一次(2min)。高峰時內存使用量上去后,除非持續申請內存,靠閾值觸發gc已經基本不可能,而是要等最多2min主動gc開始才能觸發 gc。第二個原因是go語言在向系統交還內存時只是告訴系統這些內存不需要使用了,可以回收;同時操作系統會采取“拖延癥”策略,并不是立即回收,而是等 到系統內存緊張時才會開始回收這樣該程序又重新申請內存時就可以獲得極快的分配速度。
  2. gc時間長的問題。 對于對用戶響應事件有要求的后端程序,golang gc時的stop the world兼職是噩夢。根據上文的介紹,1.5版本的go再完成上述改進后應該gc性能會提升不少,但是所有的垃圾回收型語言都難免在gc時面臨性能下 降,對此我們對于應該盡量避免頻繁創建臨時堆對象(如&abc{}, new, make等)以減少垃圾收集時的掃描時間,對于需要頻繁使用的臨時對象考慮直接通過數組緩存進行重用;很多人采用cgo的方法自己管理內存而繞開垃圾收 集,這種方法除非迫不得已個人是不推薦的(容易造成不可預知的問題),當然迫不得已的情況下還是可以考慮的,這招帶來的效果還是很明顯的~
  3. goroutine泄露的問題。 我們的一個服務需要處理很多長連接請求,實現時,對于每個長連接請求各開了一個讀取和寫入協程,全部采用endless for loop不停地處理收發數據。當連接被遠端關閉后,如果不對這兩個協程做處理,他們依然會一直運行,并且占用的channel也不會被釋放…這里就必須十 分注意,在不使用協程后一定要把他依賴的channel close并通過再協程中判斷channel是否關閉以保證其退出。
 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!