圖解 Golang 垃圾回收機制!

Go 的 GC 常常因爲性能問題被業界所詬病,下面跟着超超來看看內存垃圾是如何產生的,以及 Go 從 1.3 到 1.8 在 GC 上做了哪些改進吧!

內存垃圾

面試官:你知道程序的垃圾是怎麼產生的嗎?

考點:Go 內存管理

超超:程序在內存上被分爲堆區、棧區、全局數據區、代碼段、數據區五個部分。對於 C++ 等早期編程語言棧上的內存由編譯器管理回收,堆上的內存空間需要編程人員負責申請與釋放。在 Go 中棧上內存仍由編譯器負責管理回收,而堆上的內存由編譯器和垃圾收集器負責管理回收,給編程人員帶來了極大的便利性。

垃圾是指程序向堆棧申請的內存空間,隨着程序的運行已經不再使用這些內存空間,這時如果不釋放他們就會造成垃圾也就是內存泄漏。

例如下面這段程序

package main

//假設每個人都擁有自己都一部手機
type Person struct {
    phone *Phone
}

type Phone struct {
    money int
}

func main() {
    //定義一個Person爲超超
    chao := new(Person)

    //超超一開始用的是iphone12
    iphone := &Phone{money: 6599}
    chao.phone = iphone

    //華爲推出了鴻蒙,於是超超果斷入了一部mate40
    huawei := &Phone{money: 5899}
    chao.phone = huawei

}

隨着超超將手機從 iPhone 換成了華爲,phone所指向的內存空間就變成了垃圾,這時就需要對phone指向的內存空間進行回收,否則就變成了內存泄漏。

Go 的垃圾回收機制

面試官:那你來給我說說 Go 語言是如何實現 GC 的吧!

考點:GC 的實現

超超:那我從 Go1.3 開始說起吧,

Go1.3 使用的是標記清除法,分下面四步進行

  1. 進行 STW(stop the worl 即暫停程序業務邏輯),然後從 main 函數開始找到不可達的內存佔用和可達的內存佔用

  2. 開始標記,程序找出可達內存佔用並做標記

  3. 標記結束清除未標記的內存佔用

  4. 結束 STW 停止暫停,讓程序繼續運行,循環該過程直到 main 生命週期結束

一開始的做法是將垃圾清理結束時才停止 STW,後來優化了方案將清理垃圾放到了 STW 之後,與程序運行同時進行,這樣做減小了 STW 的時長。但是 STW 會暫停用戶邏輯對程序的性能影響是非常大的,這種粒度的 STW 對於性能較高的程序還是無法接受,因此 Go1.5 採用了三色標記法優化了 STW。

Go1.5 三色標記法

三色標記算法將程序中的對象分成白色、黑色和灰色三類。白色對象表示暫無對象引用的潛在垃圾,其內存可能會被垃圾收集器回收;灰色對象表示活躍的對象,黑色到白色的中間狀態,因爲存在指向白色對象的外部指針,垃圾收集器會掃描這些對象的子對象;黑色對象表示活躍的對象,包括不存在引用外部指針的對象以及從根對象可達的對象。

三色標記法分五步進行

  1. 將所有對象標記爲白色

  2. 從根節點集合出發,將第一次遍歷到的節點標記爲灰色放入集合列表中

  3. 遍歷灰色集合,將灰色節點遍歷到的白色節點標記爲灰色,並把灰色節點標記爲黑色

  4. 循環這個過程

  5. 直到灰色節點集合爲空,回收所有的白色節點

這種方法看似很好,但是將 GC 和程序會放一起執行,會因爲 cpu 的調度出現下面這種情況,導致被引用的對象 3 會被垃圾回收掉,從而出現錯誤。

分析 bug 的根源所在,主要是因爲程序在運行過程中出現了下面倆種情況

  1. 一個白色對象被黑色對象引用

  2. 灰色對象與它之間的可達關係的白色對象遭到破壞

因此在此基礎上拓展出了倆種方法,強三色不變式和弱三色不變式

  1. 強三色不變式:不允許黑色對象引用白色對象

  2. 弱三色不變式:黑色對象可以引用白色,白色對象存在其他灰色對象對他的引用,或者他的鏈路上存在灰色對象

爲了實現這倆種不變式的設計思想,從而引出了屏障機制,即在程序的執行過程中加一個判斷機制,滿足判斷機制則執行回調函數。

屏障機制分爲插入屏障和刪除屏障,插入屏障實現的是強三色不變式,刪除屏障則實現了弱三色不變式。值得注意的是爲了保證棧的運行效率,屏障只對堆上的內存對象啓用,棧上的內存會在 GC 結束後啓用 STW 重新掃描。

插入屏障:對象被引用時觸發的機制,當白色對象被黑色對象引用時,白色對象被標記爲灰色(棧上對象無插入屏障)。

缺點在於:如果對象 1 在棧上新創建了一個對象 6,由於棧沒有屏障機制,所以對象 6 仍爲白色節點會被回收

所以棧在 GC 迭代結束時(沒有灰色節點),會對棧執行 STW,重新進行掃描清除白色節點。(STW 時間爲 10-100ms)

刪除屏障:對象被刪除時觸發的機制。如果灰色對象引用的白色對象被刪除時,那麼白色對象會被標記爲灰色。

缺點:這種做法回收精度較低,一個對象即使被刪除仍可以活過這一輪再下一輪被回收。(如果對象 4 沒有引用對象 3,此時對象 3 應該作爲垃圾被回收,但是對象 3 卻要等到下一輪 GC 纔會被回收)

同樣也存在對棧的二次掃描影響程序的效率。

Go1.8 三色標記 + 混合寫屏障

基於插入寫屏障和刪除寫屏障在結束時需要 STW 來重新掃描棧,所帶來的性能瓶頸,Go 在 1.8 引入了混合寫屏障的方式實現了弱三色不變式的設計方式,混合寫屏障分下面四步

  1. GC 開始時將棧上可達對象全部標記爲黑色(不需要二次掃描,無需 STW)

  2. GC 期間,任何棧上創建的新對象均爲黑色

  3. 被刪除引用的對象標記爲灰色

  4. 被添加引用的對象標記爲灰色

下面爲混合寫屏障過程

面試官:這個 GC 什麼時候會被觸發呢?

考點:GC 細節

超超:觸發 GC 有倆個條件,一是堆內存的分配達到控制器計算的觸發堆大小,初始大小環境變量 GOGC,之後堆內存達到上一次垃圾收集的 2 倍時纔會觸發 GC。二是如果一定時間內沒有觸發,就會觸發新的循環,該觸發條件由runtime.forcegcperiod變量控制,默認爲 2 分鐘。

面試官:說到這那我們再聊一下 TCMalloc 算法吧。

超超:好的(自動 GC 用的舒服,面試好難呀😶‍🌫️

未完待續~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RSW0tAWsrILivqA7r-LlIA