Golang——垃圾回收GC


Go 垃圾回收原理

Golang源碼探索(三) GC的實現原理

  • 引用計數:對每個對象維護一個引用計數,當引用該對象的對象被銷毀時,引用計數減1,當引用計數器為0是回收該對象。
    • 優點:對象可以很快的被回收,不會出現內存耗盡或達到某個閥值時才回收。
    • 缺點:不能很好的處理循環引用,而且實時維護引用計數,有也一定的代價。
    • 代表語言:Python、PHP、Swift
  • 標記-清除:從根變量開始遍歷所有引用的對象,引用的對象標記為"被引用",沒有被標記的進行回收。
    • 優點:解決了引用計數的缺點。
    • 缺點:需要STW,即要暫時停掉程序運行。
    • 代表語言:Golang(其采用三色標記法)
  • 分代收集:按照對象生命周期長短划分不同的代空間,生命周期長的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收頻率。
    • 優點:回收性能好
    • 缺點:算法復雜
    • 代表語言: JAVA

標記

在之前的Go語言——內存管理一文中,分析過span是內存管理的最小單位,所以猜測gc的粒度也是span。

type mspan struct {
    // allocBits and gcmarkBits hold pointers to a span's mark and
    // allocation bits. The pointers are 8 byte aligned.
    // There are three arenas where this data is held.
    // free: Dirty arenas that are no longer accessed
    //       and can be reused.
    // next: Holds information to be used in the next GC cycle.
    // current: Information being used during this GC cycle.
    // previous: Information being used during the last GC cycle.
    // A new GC cycle starts with the call to finishsweep_m.
    // finishsweep_m moves the previous arena to the free arena,
    // the current arena to the previous arena, and
    // the next arena to the current arena.
    // The next arena is populated as the spans request
    // memory to hold gcmarkBits for the next GC cycle as well
    // as allocBits for newly allocated spans.
    //
    // The pointer arithmetic is done "by hand" instead of using
    // arrays to avoid bounds checks along critical performance
    // paths.
    // The sweep will free the old allocBits and set allocBits to the
    // gcmarkBits. The gcmarkBits are replaced with a fresh zeroed
    // out memory.
    allocBits  *gcBits
    gcmarkBits *gcBits
}

  

bitmap

如圖所示,通過gcmarkBits位圖標記span的塊是否被引用。對應內存分配中的bitmap區。

三色標記

  • 灰色:對象已被標記,但這個對象包含的子對象未標記
  • 黑色:對象已被標記,且這個對象包含的子對象也已標記,gcmarkBits對應的位為1(該對象不會在本次GC中被清理)
  • 白色:對象未被標記,gcmarkBits對應的位為0(該對象將會在本次GC中被清理)

例如,當前內存中有A~F一共6個對象,根對象a,b本身為棧上分配的局部變量,根對象a、b分別引用了對象A、B, 而B對象又引用了對象D,則GC開始前各對象的狀態如下圖所示:

  1. 初始狀態下所有對象都是白色的。
  2. 接着開始掃描根對象a、b; 由於根對象引用了對象A、B,那么A、B變為灰色對象,接下來就開始分析灰色對象,分析A時,A沒有引用其他對象很快就轉入黑色,B引用了D,則B轉入黑色的同時還需要將D轉為灰色,進行接下來的分析。
  3. 灰色對象只有D,由於D沒有引用其他對象,所以D轉入黑色。標記過程結束
  4. 最終,黑色的對象會被保留下來,白色對象會被回收掉。
 
 

STW

stop the world是gc的最大性能問題,對於gc而言,需要停止所有的內存變化,即停止所有的goroutine,等待gc結束之后才恢復。

觸發

  • 閾值:默認內存擴大一倍,啟動gc
  • 定期:默認2min觸發一次gc,src/runtime/proc.go:forcegcperiod
  • 手動:runtime.gc()

go

  1. Sweep Termination: 對未清掃的span進行清掃, 只有上一輪的GC的清掃工作完成才可以開始新一輪的GC
  2. Mark: 掃描所有根對象, 和根對象可以到達的所有對象, 標記它們不被回收
  3. Mark Termination: 完成標記工作, 重新掃描部分根對象(要求STW)
  4. Sweep: 按標記結果清掃span

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

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

需要注意的是, 不是所有根對象的掃描都需要STW, 例如掃描棧上的對象只需要停止擁有該棧的G.
從go 1.9開始, 寫屏障的實現使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間.

寫屏障

因為go支持並行GC, GC的掃描和go代碼可以同時運行, 這樣帶來的問題是GC掃描的過程中go代碼有可能改變了對象的依賴樹。

例如開始掃描時發現根對象A和B, B擁有C的指針。

  1. GC先掃描A,A放入黑色
  2. B把C的指針交給A
  3. GC再掃描B,B放入黑色
  4. C在白色,會回收;但是A其實引用了C。

為了避免這個問題, go在GC的標記階段會啟用寫屏障(Write Barrier).

啟用了寫屏障(Write Barrier)后,

  1. GC先掃描A,A放入黑色
  2. B把C的指針交給A
  3. 由於A在黑色,所以C放入灰色
  4. C沒有子對象,放入黑色
  5. 掃描B,B沒有子對象,放入黑色

即使A可能會在稍后丟掉C, 那么C就在下一輪回收。

開啟寫屏障之后,當指針發生改變, GC會認為在這一輪的掃描中這個指針是存活的, 所以放入灰色

root

首先標記root跟對象,根對象的子對象也是存活的。

根對象包括:全局變量,各個G stack上的變量等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM