1. 程式人生 > 其它 >GO語言的垃圾回收

GO語言的垃圾回收

GC 演算法有四種:

  • 引用計數(reference counting)
  • 標記-清除(mark & sweep)
  • 節點複製(Copying Garbage Collection)
  • 分代收集(Generational Garbage Collection)。

引用計數(reference counting)

引用計數的思想:每個單元維護一個域,儲存其它單元指向它的引用數量(類似有向圖的入度)。當引用數量為0時,將其回收。引用計數是漸進式的,能夠將記憶體管理的開銷分佈到整個程式之中。C++ 的 share_ptr 使用的就是引用計算方法。

引用計數演算法實現一般是把所有的單元放在一個單元池裡,比如類似 free list。這樣所有的單元就被串起來了,就可以進行引用計數了。新分配的單元計數值被設定為 1(注意不是 0,因為申請一般都說 ptr = new object 這種)。每次有一個指標被設為指向該單元時,該單元的計數值加 1;而每次刪除某個指向它的指標時,它的計數值減 1。 當其引用計數為 0 的時候,該單元會被進行回收。雖然這裡說的比較簡單,實現的時候還是有很多細節需要考慮,比如刪除某個單元的時候,那麼它指向的所有單元都需要對引用計數減 1。

優點

漸進式。記憶體管理與使用者程式的執行交織在一起,將 GC 的代價分散到整個程式。不像標記-清掃演算法需要 STW (Stop The World,GC 的時候掛起使用者程式)。

演算法易於實現。

記憶體單元能夠很快被回收。相比於其他垃圾回收演算法,堆被耗盡或者達到某個閾值才會進行垃圾回收。

缺點

原始的引用計數不能處理迴圈引用。大概這是被詬病最多的缺點了。不過針對這個問題,也除了很多解決方案,比如強引用等。

維護引用計數降低執行效率。記憶體單元的更新刪除等都需要維護相關的記憶體單元的引用計數,相比於一些追蹤式的垃圾回收演算法並不需要這些代價。

單元池 free list 實現的話不是 cache-friendly 的,這樣會導致頻繁的 cache miss,降低程式執行效率。

標記-清除(mark & sweep)

標記-清除演算法是第一種自動記憶體管理,基於追蹤的垃圾收集演算法。演算法思想在 70 年代就提出了,是一種非常古老的演算法。記憶體單元並不會在變成垃圾立刻回收,而是保持不可達狀態,直到到達某個閾值或者固定時間長度。這個時候系統會掛起使用者程式,也就是 STW,轉而執行垃圾回收程式。 垃圾回收程式對所有的存活單元進行一次全域性遍歷確定哪些單元可以回收。演算法分兩個部分:標記(mark)和清除(sweep)。標記階段表明所有的存活單元,清除階段將垃圾單元回收。

標記-清除演算法的優點也就是基於追蹤的垃圾回收演算法具有的優點:避免了引用計數演算法的缺點(不能處理迴圈引用,需要維護指標)。缺點也很明顯,需要 STW。

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

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

三色法標記主要是第一部分是掃描所有物件進行三色標記,標記為黑色、灰色和白色,標記完成後只有黑色和白色物件,黑色代表使用中物件,白色物件代表垃圾,灰色是白色過渡到黑色的中間臨時狀態,第二部分是清掃垃圾,即清理白色物件。

三色標記的一個明顯好處是能夠讓使用者程式和 mark 併發的進行.

為什麼需要三色標記?

三色標記的目的,主要是利用Tracing GC(Tracing GC 是垃圾回收的一個大類,另外一個大類是引用計數)做增量式垃圾回收,降低最大暫停時間。原生Tracing GC只有黑色和白色,沒有中間的狀態,這就要求GC掃描過程必須一次性完成,得到最後的黑色和白色物件。在前面增量式GC中介紹到了,這種方式會存在較大的暫停時間。

三色標記增加了中間狀態灰色,增量式GC執行過程中,應用執行緒的執行可能改變了物件引用樹,只要讓黑色物件直接引用白色物件,GC就可以增量式的執行,減少停頓時間。

什麼是三色標記?

三色標記,通過字面意思我們就可以知道它由3種顏色組成:

  1. 黑色 Black:表示物件是可達的,即使用中的物件,黑色是已經被掃描的物件。

  2. 灰色 Gary:表示被黑色物件直接引用的物件,但還沒對它進行掃描。

  3. 白色 White:白色是物件的初始顏色,如果掃描完成後,物件依然還是白色的,說明此物件是垃圾物件。

三色標記規則:黑色不能指向白色物件。即黑色可以指向灰色,灰色可以指向白色。

三色標記法,主要流程如下:

  1. 初始所有物件被標記為白色。

  2. 從 root 開始找到所有可達物件,標記為灰色,放入待處理佇列。

  3. 遍歷灰色物件佇列,將其引用物件標記為灰色放入待處理佇列,自身標記為黑色。

  4. 處理完灰色物件佇列,直到沒有灰色物件。

  5. 剩餘白色物件為垃圾物件,執行清掃工作。

節點複製(Copying Garbage Collection)

節點複製也是基於追蹤的演算法。其將整個堆等分為兩個半區(semi-space),一個包含現有資料,另一個包含已被廢棄的資料。節點複製式垃圾收集從切換(flip)兩個半區的角色開始,然後收集器在老的半區,也就是 Fromspace 中遍歷存活的資料結構,在第一次訪問某個單元時把它複製到新半區,也就是 Tospace 中去。 在 Fromspace 中所有存活單元都被訪問過之後,收集器在 Tospace 中建立一個存活資料結構的副本,使用者程式可以重新開始運行了。

  • 優點
  1. 所有存活的資料結構都縮並地排列在 Tospace 的底部,這樣就不會存在記憶體碎片的問題
  2. 獲取新記憶體可以簡單地通過遞增自由空間指標來實現。
  • 缺點
  1. 記憶體得不到充分利用,總有一半的記憶體空間處於浪費狀態。

分代收集(Generational Garbage Collection)

基於追蹤的垃圾回收演算法(標記-清掃、節點複製)一個主要問題是在生命週期較長的物件上浪費時間(長生命週期的物件是不需要頻繁掃描的)。同時,記憶體分配存在這麼一個事實 “most object die young”。基於這兩點,分代垃圾回收演算法將物件按生命週期長短存放到堆上的兩個(或者更多)區域,這些區域就是分代(generation)。對於新生代的區域的垃圾回收頻率要明顯高於老年代區域。

分配物件的時候從新生代裡面分配,如果後面發現物件的生命週期較長,則將其移到老年代,這個過程叫做 promote。隨著不斷 promote,最後新生代的大小在整個堆的佔用比例不會特別大。收集的時候集中主要精力在新生代就會相對來說效率更高,STW 時間也會更短。

  • 優點
  1. 效能更優。
  • 缺點
  1. 實現複雜。

GC 觸發條件

GC有3種觸發方式:

  1. 輔助GC,在分配記憶體時,會判斷當前的Heap記憶體分配量是否達到了觸發一輪GC的閾值(每輪GC完成後,該閾值會被動態設定),如果超過閾值,則啟動一輪GC。
  1. 呼叫runtime.GC()強制啟動一輪GC。

  2. sysmon是執行時的守護程序,當超過 forcegcperiod (2分鐘)沒有執行GC會啟動一輪GC。