golang垃圾回收


常見GC算法

我總結了一下常見的 GC 算法。分別是:引用計數法、Mark-Sweep法、三色標記法、分代收集法。

 

1. 引用計數法

原理是在每個對象內部維護一個整數值,叫做這個對象的引用計數,當對象被引用時引用計數加一,當對象不被引用時引用計數減一。當引用計數為 0 時,自動銷毀對象。

目前引用計數法主要用在 c++ 標准庫的 std::shared_ptr 、微軟的 COM 、Objective-C 和 PHP 中。

但是引用計數法有個缺陷就是不能解決循環引用的問題。循環引用是指對象 A 和對象 B 互相持有對方的引用。這樣兩個對象的引用計數都不是 0 ,因此永遠不能被收集。

另外的缺陷是,每次對象的賦值都要將引用計數加一,增加了消耗。

這種GC算法把業務代碼與GC算法耦合在一起,GC會導致業務代碼執行性能下降,變量指向變動越頻繁,GC占用性能越高。

 

2. Mark-Sweep法(標記清除法)

這個算法分為兩步,標記和清除。

  • 標記:從程序的根節點開始, 遞歸地遍歷所有對象,將能遍歷到的對象打上標記。
  • 清除:講所有未標記的的對象當作垃圾銷毀。 

Animation_of_the_Naive_Mark_and_Sweep_Garbage_Collector_Algorithm.gif-143.9kB 
圖片來自 https://en.wikipedia.org/wiki/Tracing_garbage_collection  

但是這個算法也有一個缺陷,就是人們常常說的 STW 問題(Stop The World)。因為算法在標記時必須暫停整個程序,否則其他線程的代碼可能會改變對象狀態,從而可能把不應該回收的對象當做垃圾收集掉。

當程序中的對象逐漸增多時,遞歸遍歷整個對象樹會消耗很多的時間,在大型程序中這個時間可能會是毫秒級別的。讓所有的用戶等待幾百毫秒的 GC 時間這是不能容忍的。

golang 1.5以前使用的這個算法。

 

3. 三色標記法

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

原理如下,

  1. 首先創建三個集合:白、灰、黑。
  2. 將所有對象放入白色集合中。
  3. 然后從根節點開始遍歷所有對象(注意這里並不遞歸遍歷),把遍歷到的對象從白色集合放入灰色集合。
  4. 之后遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合,之后將此灰色對象放入黑色集合
  5. 重復 4 直到灰色中無任何對象
  6. 通過write-barrier檢測對象有變化,重復以上操作
  7. 收集所有白色對象(垃圾) 

Animation_of_tri-color_garbage_collection.gif-94kB 
圖片來自 https://en.wikipedia.org/wiki/Tracing_garbage_collection 

這個算法可以實現 "on-the-fly",也就是在程序執行的同時進行收集,並不需要暫停整個程序(后面會講具體GC與業務代碼怎么並發執行的,其實還是會有短暫的STW的)。

但是也會有一個缺陷,三色標記法是增量GC算法,可能程序中的垃圾產生的速度會大於垃圾收集的速度,這樣會導致程序中的垃圾越來越多無法被收集掉。

使用這種算法的是 Go 1.5、Go 1.6。

 

4. 分代收集

分代收集也是傳統 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 。

 

golang的GC

root

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

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

 

標記

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

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結束之后才恢復。

 

go垃圾回收觸發方式

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

 

GC流程

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

  1. Stack scan:Collect pointers from globals and goroutine stacks。收集根對象(全局變量,和G stack),開啟寫屏障。全局變量、開啟寫屏障需要STW,G stack只需要停止該G就好,時間比較少。
  2. Mark: Mark objects and follow pointers。標記所有根對象, 和根對象可以到達的所有對象不被回收。
  3. Mark Termination: Rescan globals/changed stack, finish mark。重新掃描全局變量,和上一輪改變的stack(寫屏障),完成標記工作。這個過程需要STW。
  4. 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的時間.



寫屏障

因為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)后,在GC第三輪rescan階段,根據寫屏障標記將C放入灰色,防止C丟失。

更多並發mark會導致的問題請看https://www.cnblogs.com/qqmomery/p/6661574.html?utm_source=tuicool&utm_medium=referral

 

golang的GC演變史

go 語言在 1.3 以前,使用的是比較蠢的傳統 Mark-Sweep 算法。

1.3 版本進行了一下改進,把 Sweep 改為了並行操作。

1.5 版本進行了較大改進,使用了三色標記算法。go 1.5 在源碼中的解釋是“非分代的、非移動的、並發的、三色的標記清除垃圾收集器”。

從1.8以后的golang將第一步的stop the world 也取消了,這又是一次優化。

1.9開始, 寫屏障的實現使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間。

 

go 除了標准的三色收集以外,還有一個輔助回收功能,防止垃圾產生過快手機不過來的情況。這部分代碼在 runtime.gcAssistAlloc 中。

但是 golang 並沒有分代收集,所以對於巨量的小對象還是很苦手的,會導致整個 mark 過程十分長,在某些極端情況下,甚至會導致 GC 線程占據 50% 以上的 CPU。

因此,當程序由於高並發等原因造成大量小對象的gc問題時,最好可以使用 sync.Pool 等對象池技術,避免大量小對象加大 GC 壓力。

 

參考並引用自以下資料:

https://blog.csdn.net/erlib/article/details/51850912(為Go語言GC正名-20秒到100微妙的演變史)

https://www.cnblogs.com/diegodu/p/9150840.html(golang垃圾回收機制)

https://blog.csdn.net/liangzhiyang/article/details/52669851(golang的goroutine調度機制)

https://blog.csdn.net/bairongdong1/article/details/52216360(跟雨痕看go源碼-並發清除與三色標記)

https://www.cnblogs.com/qqmomery/p/6661574.html?utm_source=tuicool&utm_medium=referral(垃圾回收算法-三色標記法)

https://www.jianshu.com/p/8b0c0f7772da(Go語言垃圾回收GC)

 


免責聲明!

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



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