从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 版本中,我們已經(jīng)通過修改 GC 堆內(nèi)存的最大值,來避免這樣一個情況:在 docker 容器中運行的 .NET Core 程序,因為 docker 容器內(nèi)存限制而被 docker 殺死。
恰好,我在 docker swarm 集群中跑的一個程序,總是被 docker 殺死,大都是因為內(nèi)存超出了限制。那么升級到 .NET Core 3.0 是不是會起作用呢?這篇文章將淺顯的了解 .NET Core 3.0 的?Garbage Collection機制,以及 Linux 的?Cgroups?內(nèi)核功能。最后再寫一組 實驗程序 去真實的了解 .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。二者大體我覺得應(yīng)該差不多吧。所以我介紹 CLR 中的一些概念,這樣才可以更好的理解 GC
我們的程序都是在操作虛擬內(nèi)存地址,從來不直接操作內(nèi)存地址,即使是 Native Code。
一個進程會被分配一個獨立的虛擬內(nèi)存空間,我們定義的和管理的對象都在這些空間之中。
虛擬內(nèi)存空間中的內(nèi)存 有三種狀態(tài):空閑 (可以隨時分配對象),預(yù)定 (被某個進程預(yù)定,尚且不能分配對象),提交(從物理內(nèi)存中分配了地址到該虛擬內(nèi)存,這個時候才可以分配對象)CLR 初始化GC 后,GC 就在上面說的虛擬內(nèi)存空間中分配內(nèi)存,用來讓它管理和分配對象,被分配的內(nèi)存叫做?Managed Heap?管理堆,每個進程都有一個管理堆內(nèi)存,進程中的線程共享一個管理堆內(nèi)存
CLR 中還有一塊堆內(nèi)存叫做LOH?Large Object Heap 。它也是隸屬于 GC 管理,但是它很特別,只分配大于 85000byte 的對象,所以叫做大對象,為什么要這么做呢?很顯然大對象太難管理了,GC 回收大對象將很耗時,所以沒辦法,只有給這些 “大象” 另選一出房子,GC 這個“管理員” 很少管 “大象”。
那么什么時候?qū)ο髸环峙涞蕉褍?nèi)存中呢?
所有引用類型的對象,以及作為類屬性的值類型對象,都會分配在堆中。大于 85000byte 的對象扔到 “大象房” 里。
堆內(nèi)存中的對象越少,GC 干的事情越少,你的程序就越快,因為 GC 在干事的時候,程序中的其他線程都必須畢恭畢敬的站著不動(掛起),等 GC 說:我已經(jīng)清理好了。然后大家才開始繼續(xù)忙碌。所以 GC 一直都是在干幫線程擦屁股的事情。
所以沒有 GC 的編程語言更快,但是也更容易產(chǎn)生廢物。
GC Generation
那么 GC 在收拾垃圾的過程中到底做了什么呢?首先要了解 CLR 的 GC 有一個Generation?代?的概念 GC 通過將對象分為三代,優(yōu)化對象管理。GC 中的代分為三代:
Generation 0?零代或者叫做初代,初代中都是一些短命的對象,shorter object,它們通常會被很快清除。當(dāng) new 一個新對象的時候,該對象都會分配在 Generation 0 中。只有一段連續(xù)的內(nèi)存
Generation 1?一代,一代中的對象也是短命對象,它相當(dāng)于 shorter object 和 longer object 之間的緩沖區(qū)。只有一段連續(xù)的內(nèi)存
Generation 2?二代,二代中的對象都是長壽對象,他們都是從零代和一代中選拔而來,一旦進入二代,那就意味著你很安全。之前說的 LOH 就屬于二代,static 定義的對象也是直接分配在二代中。包含多段連續(xù)的內(nèi)存。
零代和一代 占用的內(nèi)存因為他們都是短暫對象,所以叫做短暫內(nèi)存塊。 那么他們占用的內(nèi)存大小是多大?32位和63位的系統(tǒng)是不一樣的,不同的GC類型也是不一樣的。
WorkStation GC:
32 位操作系統(tǒng) 16MB ,64位 操作系統(tǒng) 256M
Server GC:
32 w位操作系統(tǒng) 65MB,64 位操作系統(tǒng) 4GB!
GC 回收過程
當(dāng) 管理堆內(nèi)存中使用到達一定的閾值的時候,這個閾值是GC 決定的,或者系統(tǒng)內(nèi)存不夠用的時候,或者調(diào)用?GC.Collect()?的時候,GC 都會立刻可以開始回收,沒有商量的余地。于是所有線程都會被掛起(也并不都是這樣)
GC 會在 Generation 0 中開始巡查,如果是 死對象,就把他們的內(nèi)存釋放,如果是 活的對象,那么就標記這些對象。接著把這些活的對象升級到下一代:移動到下一代 Generation 1 中。
同理 在 Generation 1 中也是如此,釋放死對象,升級活對象。
三個 Generation 中,Generation 0 被 GC 清理的最頻繁,Generation 1 其次,Generation 2 被 GC 訪問的最少。因為要清理 Generation 2 的消耗太大了。
GC 在每一個 Generation 進行清理都要進行三個步驟:
標記: GC 循環(huán)遍歷每一個對象,給它們標記是 死對象 還是 活對象
重新分配:重新分配活對象的引用
清理:將死對象釋放,將活對象移動到下一代中
WorkStation GC 和 Server GC
GC 有兩種形式:WorkStation GC和?Server GC
默認的.NET 程序都是 WorkStation GC ,那么 WorkStation GC 和 Server GC 有什么區(qū)別呢。
上面已經(jīng)提到一個區(qū)別,那就是 Server GC 的 Generation 內(nèi)存更大,64位操作系統(tǒng) Generation 0 的大小居然有4G ,這意味著啥?在不調(diào)用GC.Collect?的情況下,4G 塞滿GC 才會去回收。那樣性能可是有很大的提升。但是一旦回收了,4GB 的“垃圾” 也夠GC 喝一壺的了。
還有一個很大的區(qū)別就是,Server GC 擁有專門用來處理 GC的線程,而WorkStation GC 的處理線程就是你的應(yīng)用程序線程。WorkStation 形式下,GC 開始,所有應(yīng)用程序線程掛起,GC選擇最后一個應(yīng)用程序線程用來跑GC,直到GC 完成。所有線程恢復(fù)。
而ServerGC 形式下: 有幾核 CPU ,那么就有幾個專有的線程來處理 GC。每個線程都一個堆進行GC ,不同的堆的對象可以相互引用。
所以在GC 的過程中,Server GC 比 WorkStation GC 更快。但是有專有線程,并不代表可以并行GC 哦。
上面兩個區(qū)別,決定了 Server GC 用于對付高吞吐量的程序,而WorkStation GC 用于一般的客戶端程序足以。
如果你的.NET 程序正在疲于應(yīng)付 高并發(fā),不妨開啟 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 模式或者關(guān)閉 Concurrent GC 模式。
Concurrent GC 當(dāng)然是為了解決上述 GC 過程中所有線程掛起等待 GC 完成的問題。因為工作線程掛起將會影響 用戶交互的流暢性和響應(yīng)速度。
Concurrent 并行實際上 只發(fā)生在Generation 2 中,因為 Generation 0 和 Generation1 的處理是在太快了,相當(dāng)于工作線程沒有阻塞。
在 GC 處理 Generation 2 中的第一步,也就是標記過程中,工作線程是可以同步進行的,工作線程仍然可以在 Generation 0 和 Generation 1 中分配對象。
所以并行 GC 可以減少工作進程因為GC 需要掛起的時間。但是與此同時,在標記的過程中工作進程也可以繼續(xù)分配對象,所以GC占用的內(nèi)存可能更多。
而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 的區(qū)別是什么呢?在閱讀很多資料后,終于搞清楚了,因為英語水平不好。以下內(nèi)容比較重要。
首先:Background GC 和 Concurrent GC 都是為了減少 因為 GC 而掛起工作線程的時間,從而提升用戶交互體驗,程序響應(yīng)速度。
其次: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 中進行分配對象,但是這是有限制的,當(dāng) Generation 0/1 中的內(nèi)存片段 Segment 用完的時候,就不能再分配了,知道 Concurrent GC 完成。而 Background GC 沒有這個限制,為啥呢?因為 Background GC 在 Generation 2 中進行清理時,允許了 Generation 0/1 進行清理,也就說是當(dāng) Generation 0/1 的 Segment 用完的時候, GC 可以去清理它們,這個GC 稱作?Foreground GC?( 前臺GC ) ,Foreground GC 清理完之后,工作線程就可以繼續(xù)分配對象了。
所以 Background GC 比 Concurrent GC 減少了更多 工作線程暫停的時間。
GC 的簡單概念就到這里了以上是閱讀大量英文資料的精短總結(jié),如果有寫錯的地方還請斧正。
作為最后一句總結(jié)GC的話:并不是使用了 Background GC 和 Concurrent GC 的程序運行速度就快,它們只是提升了用戶交互的速度。因為 專有的GC 線程會對CPU 造成拖累,此外GC 的同時,工作線程分配對象 和正常的時候分配對象 是不一樣的,它會對性能造成拖累。
.NET Core 3.0 的變化
堆內(nèi)存的大小進行了限制:max (20mb , 75% of memory limit on the container)
ServerGC 模式下 默認的Segment 最小是16mb, 一個堆 就是 一個segment。這樣的好處可以舉例來說明,比如32核服務(wù)器,運行一個內(nèi)存限制32 mb的程序,那么在Server GC 模式下,會分配32個Heap,每個Heap 大小是1mb。但是現(xiàn)在,只需要分配2個Heap,每個Heap 大小16mb。
其他的就不太了解了。
實際體驗
從開頭的 介紹 ASP.NET Core 3.0 文章中了解到 ,在 Docker 中,對容器的資源限制是通過 cgroup 實現(xiàn)的。cgroup 是 Linux 內(nèi)核特性,它可以限制 進程組的 資源占用。當(dāng)容器使用的內(nèi)存超出docker的限制,docker 就會將改容器殺死。在之前 .NET Core 版本中,經(jīng)常出現(xiàn) .NET Core 應(yīng)用程序消耗內(nèi)存超過了docker 的 內(nèi)存限制,從而導(dǎo)致被殺死。而在.NET Core 3.0 中這個問題被解決了。
為此我做了一個實驗。
這是一段代碼:
這段代碼是在 for 循環(huán) 分配對象。buffer = new byte[1024 * 1024]?占用了 1M 的內(nèi)存
這段代碼分別在 .NET Core 2.2 和 .NET Core 3.0 運行,完全相同的代碼。運行的內(nèi)存限制是 9mb
.NET Core 2.2 運行的結(jié)果是:
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 ,當(dāng)heap使用內(nèi)存到達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
....
運行一直正常沒問題。
二者的區(qū)別就是 .NET Core 2.2 GC 之后,堆內(nèi)存沒有減少。為什么會發(fā)生這樣的現(xiàn)象呢?
一下是我的推測,沒有具體跟蹤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大小做了限制,所以當(dāng)heap不夠用的時候,它會清理LOH,但是.NET Core 2.2 下認為heap還有很多,所以它不清理LOH ,導(dǎo)致程序被docker殺死。
我也試過將分配的對象大小設(shè)置小于 85kb, .NET Core 3.0 和.NET Core2.2 在內(nèi)存限制小于10mb都可以正常運行,這應(yīng)該是和 GC 在 Generation 0 中的頻繁清理的機制有關(guān),因為清理幾乎不消耗時間,不像 Generation 2, 所以在沒有限制GC heap的情況也可以運行。
我將上述代碼 發(fā)布到了 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
有興趣可以探討一下。
總結(jié)
.NET Core 3.0 的改動還是很大滴,以及應(yīng)該根據(jù)自己具體的應(yīng)用場景去配置GC ,讓GC 發(fā)揮最好的作用,充分利用Microsoft 給我們的權(quán)限。比如啟用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社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: asp.net core 系列之Perf
- 下一篇: asp.net core使用serilo