从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection
前言
在閱讀這篇文章:Announcing Net Core 3 Preview3的時候,我看到了這樣一個特性:
Docker and cgroup memory Limits
We concluded that the primary fix is to set a GC heap maximum significantly lower than the overall memory limit as a default behavior. In retrospect, this choice seems like an obvious requirement of our implementation. We also found that Java has taken a similar approach, introduced in Java 9 and updated in Java 10.
大概的意思呢就是在 .NET Core 3.0 版本中,我們已經通過修改 GC 堆內存的最大值,來避免這樣一個情況:在 docker 容器中運行的 .NET Core 程序,因為 docker 容器內存限制而被 docker 殺死。
恰好,我在 docker swarm 集群中跑的一個程序,總是被 docker 殺死,大都是因為內存超出了限制。那么升級到 .NET Core 3.0 是不是會起作用呢?這篇文章將淺顯的了解 .NET Core 3.0 的?Garbage Collection機制,以及 Linux 的?Cgroups?內核功能。最后再寫一組 實驗程序 去真實的了解 .NET Core 3.0 帶來的 GC 變化。
GC
CLR
.NET 程序是運行在 CLR : Common Language Runtime 之上。CLR 就像 JAVA 中的 JVM 虛擬機。CLR 包括了 JIT 編譯器,GC 垃圾回收器,CIL CLI 語言標準。
那么 .NET Core 呢?它運行在?CoreCLR 上,是屬于 .NET Core 的 Runtime。二者大體我覺得應該差不多吧。所以我介紹 CLR 中的一些概念,這樣才可以更好的理解 GC
我們的程序都是在操作虛擬內存地址,從來不直接操作內存地址,即使是 Native Code。
一個進程會被分配一個獨立的虛擬內存空間,我們定義的和管理的對象都在這些空間之中。
虛擬內存空間中的內存 有三種狀態:空閑 (可以隨時分配對象),預定 (被某個進程預定,尚且不能分配對象),提交(從物理內存中分配了地址到該虛擬內存,這個時候才可以分配對象)CLR 初始化GC 后,GC 就在上面說的虛擬內存空間中分配內存,用來讓它管理和分配對象,被分配的內存叫做?Managed Heap?管理堆,每個進程都有一個管理堆內存,進程中的線程共享一個管理堆內存
CLR 中還有一塊堆內存叫做LOH?Large Object Heap 。它也是隸屬于 GC 管理,但是它很特別,只分配大于 85000byte 的對象,所以叫做大對象,為什么要這么做呢?很顯然大對象太難管理了,GC 回收大對象將很耗時,所以沒辦法,只有給這些 “大象” 另選一出房子,GC 這個“管理員” 很少管 “大象”。
那么什么時候對象會被分配到堆內存中呢?
所有引用類型的對象,以及作為類屬性的值類型對象,都會分配在堆中。大于 85000byte 的對象扔到 “大象房” 里。
堆內存中的對象越少,GC 干的事情越少,你的程序就越快,因為 GC 在干事的時候,程序中的其他線程都必須畢恭畢敬的站著不動(掛起),等 GC 說:我已經清理好了。然后大家才開始繼續忙碌。所以 GC 一直都是在干幫線程擦屁股的事情。
所以沒有 GC 的編程語言更快,但是也更容易產生廢物。
GC Generation
那么 GC 在收拾垃圾的過程中到底做了什么呢?首先要了解 CLR 的 GC 有一個Generation?代?的概念 GC 通過將對象分為三代,優化對象管理。GC 中的代分為三代:
Generation 0?零代或者叫做初代,初代中都是一些短命的對象,shorter object,它們通常會被很快清除。當 new 一個新對象的時候,該對象都會分配在 Generation 0 中。只有一段連續的內存
Generation 1?一代,一代中的對象也是短命對象,它相當于 shorter object 和 longer object 之間的緩沖區。只有一段連續的內存
Generation 2?二代,二代中的對象都是長壽對象,他們都是從零代和一代中選拔而來,一旦進入二代,那就意味著你很安全。之前說的 LOH 就屬于二代,static 定義的對象也是直接分配在二代中。包含多段連續的內存。
零代和一代 占用的內存因為他們都是短暫對象,所以叫做短暫內存塊。 那么他們占用的內存大小是多大?32位和63位的系統是不一樣的,不同的GC類型也是不一樣的。
WorkStation GC:
32 位操作系統 16MB ,64位 操作系統 256M
Server GC:
32 w位操作系統 65MB,64 位操作系統 4GB!
GC 回收過程
當 管理堆內存中使用到達一定的閾值的時候,這個閾值是GC 決定的,或者系統內存不夠用的時候,或者調用?GC.Collect()?的時候,GC 都會立刻可以開始回收,沒有商量的余地。于是所有線程都會被掛起(也并不都是這樣)
GC 會在 Generation 0 中開始巡查,如果是 死對象,就把他們的內存釋放,如果是 活的對象,那么就標記這些對象。接著把這些活的對象升級到下一代:移動到下一代 Generation 1 中。
同理 在 Generation 1 中也是如此,釋放死對象,升級活對象。
三個 Generation 中,Generation 0 被 GC 清理的最頻繁,Generation 1 其次,Generation 2 被 GC 訪問的最少。因為要清理 Generation 2 的消耗太大了。
GC 在每一個 Generation 進行清理都要進行三個步驟:
標記: GC 循環遍歷每一個對象,給它們標記是 死對象 還是 活對象
重新分配:重新分配活對象的引用
清理:將死對象釋放,將活對象移動到下一代中
WorkStation GC 和 Server GC
GC 有兩種形式:WorkStation GC和?Server GC
默認的.NET 程序都是 WorkStation GC ,那么 WorkStation GC 和 Server GC 有什么區別呢。
上面已經提到一個區別,那就是 Server GC 的 Generation 內存更大,64位操作系統 Generation 0 的大小居然有4G ,這意味著啥?在不調用GC.Collect?的情況下,4G 塞滿GC 才會去回收。那樣性能可是有很大的提升。但是一旦回收了,4GB 的“垃圾” 也夠GC 喝一壺的了。
還有一個很大的區別就是,Server GC 擁有專門用來處理 GC的線程,而WorkStation GC 的處理線程就是你的應用程序線程。WorkStation 形式下,GC 開始,所有應用程序線程掛起,GC選擇最后一個應用程序線程用來跑GC,直到GC 完成。所有線程恢復。
而ServerGC 形式下: 有幾核 CPU ,那么就有幾個專有的線程來處理 GC。每個線程都一個堆進行GC ,不同的堆的對象可以相互引用。
所以在GC 的過程中,Server GC 比 WorkStation GC 更快。但是有專有線程,并不代表可以并行GC 哦。
上面兩個區別,決定了 Server GC 用于對付高吞吐量的程序,而WorkStation GC 用于一般的客戶端程序足以。
如果你的.NET 程序正在疲于應付 高并發,不妨開啟 Server GC :?https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcserver-element
Concurrent GC 和 Non-Concurrent GC
GC 有兩種模式:Concurrent?和?Non-Concurrent,也就是并行 GC 和 不并行 GC 。無論是 Server GC 還是 Concurrent GC 都可以開啟 Concurrent GC 模式或者關閉 Concurrent GC 模式。
Concurrent GC 當然是為了解決上述 GC 過程中所有線程掛起等待 GC 完成的問題。因為工作線程掛起將會影響 用戶交互的流暢性和響應速度。
Concurrent 并行實際上 只發生在Generation 2 中,因為 Generation 0 和 Generation1 的處理是在太快了,相當于工作線程沒有阻塞。
在 GC 處理 Generation 2 中的第一步,也就是標記過程中,工作線程是可以同步進行的,工作線程仍然可以在 Generation 0 和 Generation 1 中分配對象。
所以并行 GC 可以減少工作進程因為GC 需要掛起的時間。但是與此同時,在標記的過程中工作進程也可以繼續分配對象,所以GC占用的內存可能更多。
而Non-Concurrent GC 就更好理解了。
.NET 默認開啟了 Concurrent 模式,可以在?https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcconcurrent-element?進行配置
Background GC
又來了一種新的 GC 模式:?Background GC?。那么 Background GC 和 Concurrent GC 的區別是什么呢?在閱讀很多資料后,終于搞清楚了,因為英語水平不好。以下內容比較重要。
首先:Background GC 和 Concurrent GC 都是為了減少 因為 GC 而掛起工作線程的時間,從而提升用戶交互體驗,程序響應速度。
其次:Background GC 和 Concurrent GC 一樣,都是使用一個專有的GC 線程,并且都是在 Generation 2 中起作用。
最后:Background GC 是 Concurrent GC 的增強版,在.NET 4.0 之前都是默認使用 Concurrent GC 而 .NET 4.0+ 之后使用Background GC 代替了 Concurrent GC。
那么 Background GC 比 Concurrent GC 多了什么呢:
之前說到 Concurrent GC 在 Generation 2 中進行清理時,工作線程仍然可以在 Generation 0/1 中進行分配對象,但是這是有限制的,當 Generation 0/1 中的內存片段 Segment 用完的時候,就不能再分配了,知道 Concurrent GC 完成。而 Background GC 沒有這個限制,為啥呢?因為 Background GC 在 Generation 2 中進行清理時,允許了 Generation 0/1 進行清理,也就說是當 Generation 0/1 的 Segment 用完的時候, GC 可以去清理它們,這個GC 稱作?Foreground GC?( 前臺GC ) ,Foreground GC 清理完之后,工作線程就可以繼續分配對象了。
所以 Background GC 比 Concurrent GC 減少了更多 工作線程暫停的時間。
GC 的簡單概念就到這里了以上是閱讀大量英文資料的精短總結,如果有寫錯的地方還請斧正。
作為最后一句總結GC的話:并不是使用了 Background GC 和 Concurrent GC 的程序運行速度就快,它們只是提升了用戶交互的速度。因為 專有的GC 線程會對CPU 造成拖累,此外GC 的同時,工作線程分配對象 和正常的時候分配對象 是不一樣的,它會對性能造成拖累。
.NET Core 3.0 的變化
堆內存的大小進行了限制:max (20mb , 75% of memory limit on the container)
ServerGC 模式下 默認的Segment 最小是16mb, 一個堆 就是 一個segment。這樣的好處可以舉例來說明,比如32核服務器,運行一個內存限制32 mb的程序,那么在Server GC 模式下,會分配32個Heap,每個Heap 大小是1mb。但是現在,只需要分配2個Heap,每個Heap 大小16mb。
其他的就不太了解了。
實際體驗
從開頭的 介紹 ASP.NET Core 3.0 文章中了解到 ,在 Docker 中,對容器的資源限制是通過 cgroup 實現的。cgroup 是 Linux 內核特性,它可以限制 進程組的 資源占用。當容器使用的內存超出docker的限制,docker 就會將改容器殺死。在之前 .NET Core 版本中,經常出現 .NET Core 應用程序消耗內存超過了docker 的 內存限制,從而導致被殺死。而在.NET Core 3.0 中這個問題被解決了。
為此我做了一個實驗。
這是一段代碼:
這段代碼是在 for 循環 分配對象。buffer = new byte[1024 * 1024]?占用了 1M 的內存
這段代碼分別在 .NET Core 2.2 和 .NET Core 3.0 運行,完全相同的代碼。運行的內存限制是 9mb
.NET Core 2.2 運行的結果是:
GC WorkStationGCallocate number 1 objet
heap use 1 mb
GC occurs 0 times
allocate number 2 objet
heap use 2 mb
GC occurs 0 times
allocate number 3 objet
heap use 3 mb
GC occurs 0 times
allocate number 4 objet
heap use 1 mb
GC occurs 1 times
allocate number 5 objet
heap use 2 mb
GC occurs 1 times
allocate number 6 objet
heap use 3 mb
GC occurs 1 times
allocate number 7 objet
heap use 4 mb
GC occurs 2 times
allocate number 8 objet
heap use 5 mb
GC occurs 3 times
allocate number 9 objet
heap use 6 mb
GC occurs 4 times
allocate number 10 objet
heap use 7 mb
GC occurs 5 times
allocate number 11 objet
heap use 8 mb
GC occurs 6 times
allocate number 12 objet
heap use 9 mb
Exit
首先.NET Core 2.2默認使用 WorkStation GC ,當heap使用內存到達9mb時,程序就被docker 殺死了。
在.NET Core 3.0 中
GC WorkStationGCallocate number 1 objet
heap use 1 mb
GC occurs 0 times
allocate number 2 objet
heap use 2 mb
GC occurs 0 times
allocate number 3 objet
heap use 3 mb
GC occurs 0 times
allocate number 4 objet
heap use 1 mb
GC occurs 1 times
allocate number 5 objet
heap use 2 mb
GC occurs 1 times
allocate number 6 objet
heap use 3 mb
GC occurs 1 times
allocate number 7 objet
heap use 1 mb
GC occurs 2 times
allocate number 8 objet
heap use 2 mb
GC occurs 2 times
allocate number 9 objet
heap use 3 mb
GC occurs 2 times
....
運行一直正常沒問題。
二者的區別就是 .NET Core 2.2 GC 之后,堆內存沒有減少。為什么會發生這樣的現象呢?
一下是我的推測,沒有具體跟蹤GC的運行情況
首先定義的占用 1Mb 的對象,由于大于 85kb 都存放在LOH 中,Large Object Heap,前面提到過。 GC 是很少會處理LOH 的對象的, 除非是 GC heap真的不夠用了(一個GC heap包括 Large Object Heap 和 Small Object Heap)由于.NET Core 3.0 對GC heap大小做了限制,所以當heap不夠用的時候,它會清理LOH,但是.NET Core 2.2 下認為heap還有很多,所以它不清理LOH ,導致程序被docker殺死。
我也試過將分配的對象大小設置小于 85kb, .NET Core 3.0 和.NET Core2.2 在內存限制小于10mb都可以正常運行,這應該是和 GC 在 Generation 0 中的頻繁清理的機制有關,因為清理幾乎不消耗時間,不像 Generation 2, 所以在沒有限制GC heap的情況也可以運行。
我將上述代碼 發布到了 StackOverFlow 和Github 進行提問,
https://stackoverflow.com/questions/56578084/why-doesnt-heap-memory-used-go-down-after-a-gc-in-clr
https://github.com/dotnet/coreclr/issues/25148
有興趣可以探討一下。
總結
.NET Core 3.0 的改動還是很大滴,以及應該根據自己具體的應用場景去配置GC ,讓GC 發揮最好的作用,充分利用Microsoft 給我們的權限。比如啟用Server GC 對于高吞吐量的程序有幫助,比如禁用 Concurrent GC 實際上對一個高密度計算的程序是有性能提升的。
參考文章
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals
https://devblogs.microsoft.com/premier-developer/understanding-different-gc-modes-with-concurrency-visualizer/
https://devblogs.microsoft.com/dotnet/running-with-server-gc-in-a-small-container-scenario-part-1-hard-limit-for-the-gc-heap/
https://devblogs.microsoft.com/dotnet/running-with-server-gc-in-a-small-container-scenario-part-0/
https://devblogs.microsoft.com/dotnet/announcing-net-core-3-preview-3/
原文地址:https://www.cnblogs.com/dacc123/p/10980718.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: asp.net core 系列之Perf
- 下一篇: asp.net core使用serilo