1. 程式人生 > 程式設計 >搞懂Go垃圾回收

搞懂Go垃圾回收

本文主要介紹了垃圾回收的概念,Golang GC的垃圾回收演演算法和工作原理,看完本文可以讓你對Golang垃圾回收機制有個全面的理解。由於本人不瞭解其他語言的GC,並未對比其他語言的垃圾回收演演算法,需要的可以自行Google。

什麼是垃圾回收

垃圾回收(英語:Garbage Collection,縮寫為GC),在電腦科學中是一種自動的儲存器管理機制。當一個計算機上的動態儲存器不再需要時,就應該予以釋放,以讓出儲存器,這種儲存器資源管理,稱為垃圾回收。垃圾回收器可以讓程式設計師減輕許多負擔,也減少程式設計師犯錯的機會。來自維基百科

簡單地說,垃圾回收(GC)是在後臺執行一個守護執行緒,它的作用是在監控各個物件的狀態,識別並且丟棄不再使用的物件來釋放和重用資源。

go的垃圾回收

當前Golang使用的垃圾回收機制是三色標記發配合寫屏障輔助GC,三色標記法是標記-清除法的一種增強版本。

標記-清除法(mark and sweep)

原始的標記清楚法分為兩個步驟:

  1. 標記。先STP(Stop The World),暫停整個程式的全部執行執行緒,將被引用的物件打上標記
  2. 清除沒有被打標機的物件,即回收記憶體資源,然後恢復執行執行緒。

這樣做有個很大的問題就是要通過STW保證GC期間標記物件的狀態不能變化,整個程式都要暫停掉,在外部看來程式就會卡頓。

三色標記法

三色標記法是對標記階段的改進,原理如下:

  1. 初始狀態所有物件都是白色。
  2. 從root根出發掃描所有根物件(下圖a,b),將他們引用的物件標記為灰色(圖中A,B)

那麼什麼是root呢? 看了很多文章都沒解釋這這個概念,在這兒說明下:root區域主要是程式執行到當前時刻的棧和全域性資料區域。

  1. 分析灰色物件是否引用了其他物件。如果沒有引用其它物件則將該灰色物件標記為黑色(上圖中A);如果有引用則將它變為黑色的同時將它引用的物件也變為灰色(上圖中B引用了D)
  2. 重複步驟3,直到灰色物件佇列為空。此時白色物件即為垃圾,進行回收。

也可以參考下面的動圖輔助理解:

Go GC如何工作

上面介紹的是GO GC採用的三色標記演演算法,但是好像並沒有體現出來怎麼減少STW對程式的影響呢?其實是因為Golang GC的大部分處理是和使用者程式碼並行的

GC期間使用者程式碼可能會改變某些物件的狀態,如何實現GC和使用者程式碼並行呢?先看下GC工作的完整流程:

  1. Mark: 包含兩部分:
  • Mark Prepare: 初始化GC任務,包括開啟寫屏障(write barrier)和輔助GC(mutator assist),統計root物件的任務數量等。這個過程需要STW
  • GC Drains: 掃描所有root物件,包括全域性指標和goroutine(G)棧上的指標(掃描對應G棧時需停止該G),將其加入標記佇列(灰色佇列),並迴圈處理灰色佇列的物件,直到灰色佇列為空。該過程後臺並行執行
  1. Mark Termination: 完成標記工作,重新掃描(re-scan)全域性指標和棧。因為Mark和使用者程式是並行的,所以在Mark過程中可能會有新的物件分配和指標賦值,這個時候就需要通過寫屏障(write barrier)記錄下來,re-scan 再檢查一下。這個過程也是會STW的。
  2. Sweep: 按照標記結果回收所有的白色物件,該過程後臺並行執行
  3. Sweep Termination: 對未清掃的span進行清掃,只有上一輪的GC的清掃工作完成才可以開始新一輪的GC。 如果標記期間使用者邏輯改變了剛打完標記的物件的引用狀態,怎麼辦呢?

寫屏障(Write Barrier)

寫屏障:該屏障之前的寫操作和之後的寫操作相比,先被系統其它元件感知。 好難懂哦,結合上面GC工作的完整流程就好理解了,就是在每一輪GC開始時會初始化一個叫做“屏障”的東西,然後由它記錄第一次scan時各個物件的狀態,以便和第二次re-scan進行比對,引用狀態變化的物件被標記為灰色以防止丟失,將屏障前後狀態未變化物件繼續處理。

輔助GC

從上面的GC工作的完整流程可以看出Golang GC實際上把單次暫停時間分散掉了,本來程式執⾏可能是“⽤戶程式碼-->⼤段GC-->⽤戶程式碼”,那麼分散以後實際上變成了“⽤戶程式碼-->⼩段 GC-->⽤戶程式碼-->⼩段GC-->⽤戶程式碼”這樣。如果GC回收的速度跟不上使用者程式碼分配物件的速度呢? Go 語⾔如果發現掃描後回收的速度跟不上分配的速度它依然會把⽤戶邏輯暫停,⽤戶邏輯暫停了以後也就意味著不會有新的物件出現,同時會把⽤戶執行緒搶過來加⼊到垃圾回收⾥⾯加快垃圾回收的速度。這樣⼀來原來的併發還是變成了STW,還是得把⽤戶執行緒暫停掉,要不然掃描和回收沒完沒了了停不下來,因為新分配物件⽐回收快,所以這種東⻄叫做輔助回收。

如何進行GC調優

衡量GC對程式的影響可以參考這篇文章,Go 程式的效能除錯問題

減少物件的分配,合理重複利用; 避免string與[]byte轉化;

兩者發生轉換的時候,底層資料結結構會進行復制,因此導致 gc 效率會變低。

少量使用+連線 string;

Go裡面string是最基礎的型別,是一個只讀型別,針對他的每一個操作都會建立一個新的string。 如果是少量小文字拼接,用 “+” 就好;如果是大量小文字拼接,用 strings.Join;如果是大量大文字拼接,用 bytes.Buffer。

GC觸發條件

自動垃圾回收的觸發條件有兩個:

  1. 超過記憶體大小閾值
  2. 達到定時時間 閾值是由一個gcpercent的變數控制的,當新分配的記憶體佔已在使用中的記憶體的比例超過gcprecent時就會觸發。比如一次回收完畢後,記憶體的使用量為5M,那麼下次回收的時機則是記憶體分配達到10M的時候。也就是說,並不是記憶體分配越多,垃圾回收頻率越高。 如果一直達不到記憶體大小的閾值呢?這個時候GC就會被定時時間觸發,比如一直達不到10M,那就定時(預設2min觸發一次)觸發一次GC保證資源的回收。

寫在最後

雖然Golang有自動垃圾回收機制,但是GC不是萬能的,最好還是養成手動回收記憶體的習慣:比如手動把不再使用的記憶體釋放,把物件置成nil,也可以考慮在合適的時候呼叫runtime.GC()觸發GC。

近期在維護的go學習示例程式碼,新入坑的朋友們可以關注下 go-programming

參考:

string討論

Go語言——垃圾回收GC

Golang 垃圾回收剖析

Golang垃圾回收機制詳解

go垃圾回收概要

常見GC演演算法及Golang GC