前言
在上文[如何獲取GC的STW時間]一文中,我們聊到了如何通過監聽GC發出的診斷事件來計算STW時間。里面只簡單的介紹了幾種GC事件和它的流程。
群里就有小伙伴在問,那么GC事件是什么時候產生的?分別是代表什么含義?
那么在本文就通過幾個圖為大家解答一下這個問題。
有哪些GC模式?
工作站和服務器模式
在.NET中,GC其實有一些不同的工作模式,根據客戶端和服務器可以分為如下兩種模式:
Workstation GC
Workstation GC(工作站GC),這種模式主要是為了滿足基于UI的交互式應用程序設計的,交互式意味著GC的暫停時間要盡可能的短。因為我們不想因為觸發GC導致較長的GC停頓。
GC會更頻繁的發生,每次暫停時間都會很短。
內存占用率更低,因為GC更頻繁的發生,所以內存回收的更積極,占用率也會更低。
無論是否有配置多CPU核心,垃圾回收始終只使用一個CPU核心,只有一個托管堆。
內存段的大小設置會很小。
Server GC
Server GC (服務器GC),這種模式主要是為了滿足基于請求處理的WEB等類型應用程序設計的,這意味著它更側重于需要滿足大的吞吐量,零星的停頓不會對齊產生重大的影響。
GC的發生頻率會降低,優先滿足大吞吐量。
內存占用率會更高,因為GC發生的頻率變低,內存中可能會有很多垃圾對象。
垃圾回收使用高優先級運行在多個專用線程上。每個CPU核心都提供執行垃圾回收的專用線程和堆,每個CPU核心上的堆都包含小對象、大對象堆。
因為多個垃圾回收線程一起工作,所以對于相同大小的堆,Server GC會回收的更快一些。
服務器垃圾回收通常會有更大的Segment,另外也會占用更多的資源。
并發與非并發模式
另外根據GC相對于用戶線程的操作方式,還可以分為下面兩種方式:
Non-Concurrent
Non-Concurrent(非并發GC),這種方式是一直存在于.NET中的,它適用于工作站和服務器模式,在GC進行過程中,所有的用戶線程都會掛起。
Concurrent(已過時)
Concurrent (并發GC),并發GC模式它和用戶線程同時工作,GC進行過程中只有少數幾個過程需要掛起用戶線程。所以它的實現也更加復雜,但是暫停時間會更短,性能也會更好,不過現在它已經過時,本文不會著重描述它。
Background
Background(后臺GC),在.NET Framework 4.0以后,后臺GC取代了并發GC,它只適用于Gen2的回收,但是它可以觸發對于Gen0、Gen1的回收。根據WorkstationGC和ServerGC的模式會分別在一個或多個線程上執行。
GC工作流程
需要知道的GC事件
其實對于我們分析GC的工作來說,上文中提到的幾個事件已經足夠使用了,讓我們再來回顧一下這些事件。
Microsoft-Windows-DotNETRuntime/GC/SuspendEEStart //開始暫停托管線程運行Microsoft-Windows-DotNETRuntime/GC/SuspendEEStop //暫停托管線程完成Microsoft-Windows-DotNETRuntime/GC/Start // GC開始回收Microsoft-Windows-DotNETRuntime/GC/Stop // GC回收結束Microsoft-Windows-DotNETRuntime/GC/RestartEEStart //恢復之前暫停的托管線程Microsoft-Windows-DotNETRuntime/GC/RestartEEStop //恢復托管線程運行完成
圖例
為了讓大家能更清晰的看懂下面的圖,會用不同形狀和顏色的圖像來代表不同的含義,如下方所示:
綠色:正在運行的用戶線程。
紅色:執行引擎進行線程凍結或線程恢復。
實線箭頭:正在運行的GC線程。
虛線箭頭:被暫停的線程。
黃色圓球:GC事件。
紅色圓球:標記點。
WorkstationGC模式-非后臺(并發)GC
下圖是WorkStationGC(非后臺)模式的執行流程,我們假設它是在一個雙核的機器上運行(下文中都是假設在雙核機器上運行),運行過程其實就像下圖所示。
在上圖中的事件流如下所示:
GC/SuspendEEStart
GC/SuspendEEStop
GC/Start
GC/Stop
GC/RestartEEStart
GC/RestartEEStop
其中各個標記點分別完成了如下工作:
A->B:暫停所有用戶線程
B->C: 挑選一個用戶線程作為GC線程,然后開始進行垃圾回收
選擇-需要被回收的一代
標記-被回收的一代和更年輕一代對象
計劃-GC決定是需要壓縮整理堆還是只是清掃堆就夠了
清掃、搬遷和壓縮-根據上面計劃的結果,執行清掃堆,或者搬遷活著的對象然后整理堆,最后所有對象的地址更新到新地址。
C->D: GC工作結束,恢復線程運行
由于GC暫停了所有的線程,所以A->D就是此類GC的STW Time時間。
ServerGC模式-非后臺(并發)GC
下圖是ServerGC(非后臺)模式的執行流程。
它與WorkstationGC模式的事件流和完成的工作都一致,唯一不同的就是它會根據當前的CPU邏輯核心數量創建單獨的GC線程,比如上圖就有2個GC線程。
另外在服務器GC模式中,用戶線程還是可以作為GC線程來使用的,像用戶線程1在GC發生的時候就做了一些GC工作。
WorkstationGC模式-后臺GC
下圖是WorkstationGC(后臺)模式的執行流程,可以看到后臺模式還是相當復雜的,會短暫的暫停多次,每一次都會執行不同的操作。
除了工作線程GC以外,另外會有單獨的后臺GC線程進行后臺垃圾回收。
上圖中的事件流如下所示:
GC/SuspendEEStart
GC/SuspendEEStop
GC/Start
GC/RestartEEStart
GC/RestartEEStop
GC/SuspendEEStart
GC/SuspendEEStop
GC/RestartEEStart
GC/RestartEEStop
GC/SuspendEEStart
GC/SuspendEEStop
GC/Start
GC/Stop
GC/RestartEEStart
GC/RestartEEStop
GC/Stop
其中各個標記點完成的工作如下所示:
A->B:初始選擇、標記
此時用戶線程是暫停的
選擇需要被回收的一代
找到GC roots,以便并發標記
B->C:并發標記
此時用戶線程是正常運行的
從上一步中找到的GC roots開始標記需要被回收的一代和年輕的代
D->E:最終標記
此時用戶線程是暫停的
掃描在并發標記過頁面,看看是否有修改讓對象重新活過來的
F->G:清掃小對象堆
此時用戶線程是正常運行的
清掃小對象堆的對象
H->I:壓縮整理小對象堆、清掃壓縮整理大對象堆
此時用戶線程是暫停的
選擇了一個用戶線程進行GC
用來壓縮小對象堆的對象
另外也會壓縮和整理大對象堆對象
J->K:清掃大對象堆
此時用戶線程是正常運行的
此時會清掃和整理大的對象堆
此時會禁止分配大對象,阻塞對應線程直到大對象堆回收完成
從上面的的流程中可以看到,后臺GC主要是通過并發+多次短暫暫停來實現提升吞吐量和降低總體的STW Time的,其內部實現是非常復雜的,有興趣的小伙伴可以直接看dotnet/runtime/gc.cpp文件。
ServerGC模式-非后臺GC
下圖是ServerGC(后臺)模式的執行流程。
它與WorkstationGC模式的事件流和完成的工作都一致,唯一不同的就是它會根據當前的CPU邏輯核心數量創建單獨的GC線程,比如上圖就有2個GC線程,2個后臺GC線程。
總結
今天帶了解了一下.NET GC中的各個階段和事件的順序,當然這里只是簡單的帶大家了解一下,要知道在任何有runtime的平臺中,GC是其中相當關鍵的東西,大家如果對GC感興趣,可以閱讀附錄中的資料。
附錄
https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/
https://github.com/dotnet/runtime/blob/main/src/coreclr/gc/gc.cpp
https://netcoreimpl.github.io/
http://www.tup.tsinghua.edu.cn/booksCenter/book_08454701.html