Golang的垃圾回收機制

什麼是垃圾回收?

傳統的系統級編程語言(主要指C/C++)中,程序員必須對內存小心的進行管理操作,控制內存的申請及釋放。稍有不慎,就可能產生內存泄露問題,這種問題不易發現並且難以定位,一直成爲困擾開發者的噩夢。爲了解決這個問題,後來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行爲就被稱爲垃圾回收(Garbage Collection),簡稱GC

常見的垃圾回收方法

引用計數(reference counting)

對每個對象維護一個引用計數,當引用該對象的對象被銷燬或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數爲0時則立即回收對象。

  1. 優點:實現簡單,並且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛。
  2. 缺點:頻繁更新引用計數降低了性能。循環引用問題,當對象間發生循環引用時引用鏈中的對象都無法得到釋放。

標記-清除(mark and sweep)

這個算法分爲兩步,標記和清除。
標記:從程序的根節點開始, 遞歸地遍歷所有對象,將能遍歷到的對象打上標記。
清除:將所有未標記的對象當作垃圾銷燬。

缺點:是人們常常說的 STW 問題(Stop The World)。因爲算法在標記時必須暫停整個程序,否則其他線程的代碼可能會改變對象狀態,從而可能把不應該回收的對象當做垃圾收集掉。當程序中的對象逐漸增多時,遞歸遍歷整個對象樹會消耗很多的時間。在大型程序中這個時間可能會是毫秒級別的,等待時間太久了不能忍。

分代收集(generation)

分代收集是傳統 Mark-Sweep 的一個改進。這個算法是基於一個經驗:絕大多數對象的生命週期都很短。所以按照對象的生命週期長短來進行分代。

一般 GC 都會分三代:
在Java 中稱之爲新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation)。
在 .NET 中稱之爲第0代、第1代和第2代。

工作原理如下:

  1. 新對象放入第0代;
  2. 當內存用量超過一個較小的閾值時,觸發0代收集;
  3. 將第0代倖存的對象(未被收集)放入第1代;
  4. 只有當內存用量超過一個較高的閾值時,纔會觸發 1 代收集;
  5. 2代同理。

因爲 0 代中的對象十分少,所以每次收集時遍歷都會非常快(比 1 代收集快幾個數量級)。只有內存消耗過於大的時候纔會觸發較慢的 1 代和 2 代收集。

三色標記法

三色標記法是傳統 Mark-Sweep 的一個改進,它是一個併發的 GC 算法。
工作原理如下:

  1. 首先創建三個集合:白、灰、黑。
  2. 將所有對象放入白色集合中。
  3. 然後從根節點開始遍歷所有對象(注意這裏並不遞歸遍歷),把遍歷到的對象從白色集合放入灰色集合。
  4. 之後遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合,之後將此灰色對象放入黑色集合。
  5. 重複 4 直到灰色中無任何對象。
  6. 通過write-barrier檢測對象有變化,重複以上操作。
  7. 收集所有白色對象(垃圾)。

這個算法可以實現 “on-the-fly”,也就是在程序執行的同時進行收集,並不需要暫停整個程序。
但是也會有一個缺陷,可能程序中的垃圾產生的速度會大於垃圾收集的速度,這樣會導致程序中的垃圾越來越多無法被收集掉。

write-barrier(寫屏障):對於和用戶程序併發運行的垃圾回收算法,用戶程序會一直修改內存,所以需要記錄下來。

Go目前使用的就是三色標記算法。go 1.5 在源碼中的解釋是非分代的、非移動的、併發的、三色的標記清除垃圾收集器

Go的GC何時觸發

我們對後臺服務進行壓力測試時發現,我們模擬大量的用戶請求訪問後臺服務,這時各服務模塊能觀察到明顯的內存佔用上升。但是當停止壓測時,內存佔用並未發生明顯的下降。花了很長時間定位問題,使用gprof等各種方法,依然沒有發現原因。最後發現原來這時正常的…主要的原因有兩個:

  1. Go的垃圾回收有個觸發閾值,這個閾值會隨着每次內存使用變大而逐漸增大(如初始閾值是10MB則下一次就是20MB,再下一次就成爲了40MB…),如果長時間沒有觸發GC,Go會主動觸發一次(2min)。高峯時內存使用量上去後,除非持續申請內存,靠閾值觸發GC已經基本不可能,而是要等最多2min主動開始才能觸發GC。
  2. Go語言在向系統交還內存時只是告訴系統這些內存不需要使用了,可以回收;同時操作系統會採取“拖延症”策略,並不是立即回收,而是等到系統內存緊張時纔會開始回收。

Golang在GC的時候會發生Stop the world,整個程序會暫停,然後去標記整個內存裏面可以被回收的變量,標記完之後恢復程序執行,最後異步得去回收內存。一般這個過程會達到20ms。標記可回收變量的時間取決於臨時變量的個數。臨時變量數量越多,掃描時間會越長

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章