golang入門系列之Go GC

2020-10-08 13:00:48

在這裡插入圖片描述

前言

      因為之前作為興趣粗略的研究過Java的各種垃圾回收(CMS、G1、ZGC等),今天來大概瞭解一下Go的GC,如有錯誤,歡迎斧正。

一、先來了解一下常見GC演演算法

常見的 GC 演演算法。參照計數法、複製演演算法、標記-清除法、標記整理法、三色標記法、分代收集法。

1. 參照計數法

  • 原理是在每個物件內部維護一個整數值,叫做這個物件的參照計數;
  • 當物件被參照時參照計數加一,當物件不被參照時參照計數減一。當參照計數為 0
    時,自動銷燬物件。
    簡單但是速度很慢,缺陷是不能處理迴圈參照的情況。

2. 複製演演算法、標記-清除法、標記整理法

這哥仨是JVM的,今天不是主角,就簡單說說

  • 複製演演算法:一塊空間均等割裂成兩塊,存活的複製過去,另一塊清理;其實瞭解Java的都知道年輕代其實不是均等隔成兩塊,是8:1:1劃分。好處是實現簡單,執行高效,缺點也看到了,記憶體縮小為原來的一半。
  • 標記-清除法:首先標記出所有需要回收的物件(Java是通過GCRoot),在標記完成後統一回收所有被標記的物件。缺點:記憶體碎片;
  • 標記整理法:標記過程仍然與」標記-清除」演演算法一樣,但是後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界意外的記憶體。解決記憶體碎片,代價是時間。


3. 分代收集

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

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

原理如下:

新物件放入第 0 代
當記憶體用量超過一個較小的閾值時,觸發 0 代收集
第 0 代倖存的物件(未被收集)放入第 1 代
只有當記憶體用量超過一個較高的閾值時,才會觸發 1 代收集
2 代同理
因為 0 代中的物件十分少,所以每次收集時遍歷都會非常快(比 1 代收集快幾個數量級)。只有記憶體消耗過於大的時候才會觸發較慢的 1 代和 2 代收集。

因此,分代收集是目前比較好的垃圾回收方式。使用的語言(平臺)有 jvm、.NET 。


4. 三色標記法

三色標記法是傳統 Mark-Sweep 的一個改進,是一個並行的 GC 演演算法。

首先做個規定:

  1. 初始所有物件都是白色(未遍歷到)
  2. 從根出發能可達的物件是灰色
  3. 子節點遍歷完的物件是黑色

有了這個,我們來看演演算法:

  1. 初始所有物件都是白色。
  2. 從根出發掃描所有可達物件,標記為灰色,放入待處理佇列。
  3. 從佇列取出灰色物件,將其參照物件標記為灰色放入佇列,自身標記為黑色。
  4. 重複 3,直到灰色物件佇列為空。此時白色物件即為垃圾,進行回收。

像CMS和G1都是用到了這個,只是做了億點點改進,而且三色標記會產生浮動垃圾,這是不可避免的,而且CMS就是在三色標記這塊有個大bug導致STW有時候巨長,所以JDK沒用他做預設垃圾回收器,但是CMS仍然是一個承上啟下的開創性的並行垃圾回收器。

二、現在來看Go GC

1. GC流程

GO的GC是並行GC, 也就是GC的大部分處理和普通的go程式碼是同時執行的, 這讓GO的GC流程比較複雜.

  • Stack scan:Collect pointers from globals and goroutine stacks。收集根物件(全域性變數,和G stack),開啟寫屏障。全域性變數、開啟寫屏障需要STW。
  • Mark: Mark objects and follow pointers。標記所有根物件,和根物件可以到達的所有物件不被回收。
  • Mark Termination: Rescan globals/changed stack, finish mark。重新掃描全域性變數,和上一輪改變的stack(寫屏障),完成標記工作。這個過程需要STW。
  • Sweep: 按標記結果清掃span

目前整個GC流程會進行兩次STW(Stop The World), 第一次是Stack scan階段, 第二次是Mark Termination階段.

第一次STW會準備根物件的掃描, 啟動寫屏障(Write Barrier)和輔助GC(mutator assist).
第二次STW會重新掃描部分根物件, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist).

從1.8以後的golang將第一步的stop the world 也取消了,這又是一次優化;
1.9開始, 寫屏障的實現使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間.

2. GC觸發條件

主動的話,通過呼叫 runtime.GC(),這是阻塞式的。

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

  1. 超過記憶體大小閾值
  2. 達到定時時間

閾值是由一個gcpercent的變數控制的,當新分配的記憶體佔已在使用中的記憶體的比例超過gcprecent時就會觸發。

比如一次回收完畢後,記憶體的使用量為5M,那麼下次回收的時機則是記憶體分配達到10M的時候。也就是說,並不是記憶體分配越多,垃圾回收頻率越高。

如果一直達不到記憶體大小的閾值呢?這個時候GC就會被定時時間觸發,比如一直達不到10M,那就定時(預設2min觸發一次)觸發一次GC保證資源的回收。