天天看點

golang 系列:啥是垃圾回收?

摘要

golang 的三色标記法雖然沒有 java 的記憶體回收機制成熟,但它細分了回收過程,通過寫屏障技術,能和使用者程式并發進行,這也一定程度的提高了記憶體回收速度。

一、為什麼要有垃圾回收

我們都知道,當程式啟動的時候,作業系統是會配置設定出棧區和堆區的,作為動态記憶體配置設定使用。

在棧區裡配置設定的記憶體是可以自動管理的,一旦某個變量的作用域結束,就可以被自動回收了。

但是堆區就不是這樣的了,堆區是屬于程式員自己管理的區域,即使在某個作用域結束了,後續也能使用到該變量。

為此,程式員需要時刻關注記憶體的管理,否則将出現很多問題。例如記憶體一直在增長沒有釋放,則會出現記憶體溢出;記憶體釋放後還繼續通路,則會出現非法通路等。

是以,對記憶體的管理事關重要。然而,人為的管理記憶體始終存在隐患,誰也不能保證自己寫的代碼沒有一點問題。

是以垃圾回收機制出現了,它是程式設計語言的設計者在程式運作時通過一定的政策,讓閑置的記憶體被自動回收。

這也能讓開發者更加專注于業務邏輯,減少額外的負擔。

二、垃圾回收有哪些常用政策

1) 引用計數法

此算法為對象維護了一個計數值,當對象被引用時,計數值 +1,當對象的引用被釋放時計數值 -1,直到計數值為 0 ,表示沒有其他對象在使用它了,此時就可以進行回收動作了。

引用計數法算法簡單,易于實作。但頻繁的更新引用計數,也帶來了一定的開銷。而且對于循環引用的情況,計數值是歸不了 0 的,此時就做不了回收了。

2) 标記-清除法

标記清除法是對對象定時的進行标記,分為正在被使用的和沒有被使用這兩類。

當标記完後就可以對沒有被使用的這一類對象進行記憶體回收了。

标記清除法有個 Root 根對象,周遊搜尋都是從 Root 根對象開始标記的。由于不存在對象跟 Root 循環引用的情況,是以總是能搜尋到所有能達到的對象,依次标記。

如果循環對象沒有被标記到,就表示沒有被引用,就可以回收了,循環引用問題就解決了。

由于程式是動态在運作的,随時有可能會改變對象的引用指向。是以,在進行标記動作的時候需要 Stop the world(STW),也就是停止其他任務的執行,使得标記過程沒有被打亂。

這也就意味着程式會短暫的停滞,對于響應要求高的程式而言,無疑是不能接受的。

三、golang 的垃圾回收

golang 采用了叫三色标記法的回收機制,它是第二種算法的變種,通過将标記清除過程細分了多個階段,并采用了寫屏障去感覺引用的修改,使得垃圾回收動作能和使用者程式并發的進行,大大縮短了 Stop The World 的同時,也保證了對象不被誤清除。

三色标記法将對象分成了三種:

  • 白色對象:未被使用的對象;
  • 灰色對象:目前對象有引用對象,但是還沒有對引用對象繼續掃描過;
  • 黑色對象,對上面提到的灰色對象的引用對象已經全部掃描過了,下次不用再掃描它的引用對象了。

當垃圾回收開始時,Go 會把根對象标記為灰色,其他對象标記為白色,然後從根對象周遊搜尋,按照上面的定義去不斷的對灰色對象進行掃描标記。

(這裡的根對象可以了解為指向堆記憶體區塊的指針)

當沒有灰色對象時,表示标記完成,然後就可以開始清除白色對象了。

三色标記法在正式标記前會進行 Stop The World,以便啟動寫屏障。當啟動好後,就會停止 Stop The World。然後開始标記對象,在這标記過程中,是可以和使用者程式一起并發執行的。

當所有的對象都标記完,也就是沒有灰色對象可周遊搜尋時,會再一次的 STW,做第二次的掃描。

利用之前啟動的寫屏障,将标記期間有過引用修改的對象重新标記為灰色,保證對象不會被誤清。

當第二次掃描結束時,就可以開始真正的清除動作了,而且也是可以跟使用者程式一起并發執行的。

後面使用者程式即使再 new 了對象,配置設定了記憶體,也不會進行标記動作了,相當于這些新的對象是下次 GC 要處理的了。

而對于原先被标記為白色的對象,也就是再也沒有被使用的對象,程式是引用不到的了。此時就可以大膽并發清除,不需要再次 Stop the world 了。

觸發時機

Go 允許手動觸發垃圾回收,但一般開發者比較少介入記憶體的管理,更多是讓運作時 runtime 根據下面 2 種情況來進行垃圾回收:

  • 記憶體配置設定到一定大小時觸發
  • 一定時間内沒有觸發過垃圾回收,則會開始進行 GC,一般這個時間是 2 分鐘

結尾

雖然有 GC 幫我們做記憶體的管理,但資源不是無限的,一旦記憶體上漲,那我們就得學會查找問題了。

當記憶體一直沒有釋放時,我們可以使用 pprof 這些性能分析工具,幫助我們去做記憶體分析。

另外,在寫代碼時我們也可以從下面幾個點進行記憶體的優化,讓我們的程式更加健壯。

  • string 和 []byte 可以通過 unsafe 或 reflect 包進行強制轉換,以減少記憶體拷貝。
  • 如果頻繁的臨時對象需要建立,則可以使用 sync.Pool 來重用對象。減少垃圾回收。
  • 如果能提前知道 slice 的大小,盡量預配置設定好它的容量,避免不斷的 append 中,不斷的擴容。