從CLR GC到CoreCLR GC看.NET Core對雲原生的支持

內存分配概要

前段時間在園子裏看到有人提到了GC學習的重要性,很贊同他的觀點。充分了解GC能夠幫助咱們更好的認識.NET的設計以及爲什麼在雲原生開發中.NET Core會佔有更大的優點,這也是一個程序員成長到更高層次所須要經歷的過程。在認識GC的過程當中,咱們先看一下.NET中內存分配的概要知識。
.NET分配內存,主要依據託管資源和非託管資源進行分配。託管資源分配到了託管堆中並受CLR的管理,非託管資源分配到了非託管堆中。該節主要討論託管資源的分配。
CLR支持兩種基本類型:值類型和引用類型。CLR對這兩種類型在運行時有兩種分配方式:html

CLR內存操做

內存的分配過程以下圖所示git

memeryallot

須要注意的是,CLR還要維護一個指針,稱爲NextObjPtr,這個指針指向下一個對象再堆中的分配位置。初始化時,NextObjPtr設爲地址空間區域的基地址。一個區域被非垃圾對象填滿後,CLR會分配更多的區域,指針也會不斷偏移。new操做符會返回對象的引用,就在返回這個引用以前,NextObjPtr指針的值會加上對象佔用的字節數來獲得一個新值,即下一個對象放入托管堆時的地址。程序員

nextobjectptr

垃圾回收算法與GC運行機制

經常使用的垃圾回收算法主要有引用計數算法和引用跟蹤算法。引用計算有着明顯的缺陷,.NET使用的垃圾回收算法是引用跟蹤法。小記:關於垃圾回收算法,我記得有一個知識點,在C#中若是出現了循環引用是否會致使內存溢出?若是比較瞭解這兩種算法就會知道不會溢出。github

GC Root

引用跟蹤算法,經過一系列GCRoot對象做爲起始點,從這些點開始向下搜索,搜索的路徑成爲引用鏈,當一個對象到GC沒有任何引用鏈,說明對象能夠被回收。算法

GC Root能夠類比樹來解釋docker

gcroot

GC根節點存在於堆棧中,指向Teacher引用對象。它包含一個ArrayList訂單集合,由Teacher對象引用。集合自己也包含對其元素的引用,隨着搜索深度的增長,樹也不斷長大。緩存

GC根節點的引用源來自安全

(1)、堆棧
(2)、全局或靜態變量
(3)、CPU寄存器
(4)、互操做引用(COM / API調用中使用的.NET對象)
(5)、對象終結引用(objects finalization references)服務器

GC運行機制

GC引入了代的概念,分爲三種代,G0、G一、G2,G0對象生存週期較短,越日後生存週期越長(雖然G2中因爲直接存儲了大對象,又因爲G2不是每次都會掃描,因此大多數狀況下,G2中的對象的生存週期比G0中的更長)。多線程

GC運行以下圖所示

GC flow

須要注意的是,CLR想要進行垃圾回收時,會當即掛起執行託管代碼中的全部線程,正在執行非託管代碼的線程不會掛起。因此再多線程環境下,可能會出現莫名其妙的詭異問題。

下圖爲GC的總體運行流程,包含五個步驟:

gcflow

垃圾回收時機與模式

CLR會在一下狀況發生時,執行GC操做

      一、當GC的代的預算大小已經達到閾值而沒法對新對象分配空間的時候,好比GC的第0代已滿;

  二、顯式調用System.GC.Collect()(顯示調用要慎重,由於手動調用可能會與自動執行的GC衝突,從而致使沒法預知的問題);

  三、其餘特殊狀況,好比,操做系統內存不足、CLR卸載AppDomain、CLR關閉,甚至某些極端狀況下系統參數設置改變也可能致使GC回收。

關於GC模式主要有

  • WorkStation GC
  • Server GC
  • Concurrent GC
  • Non-Concurrent GC
  • Background GC

詳細信息請參閱:http://www.javashuo.com/article/p-untiwtry-ea.html,這篇文章關於GC模式的說明比較詳細。

.NET Core 3.0中的GC優化處理

.NET Core 3.0默認更好的支持Docker資源限制,官方團隊也在努力讓.NET Core成爲真正的容器運行時,使其在低內存環境中具備容器感知功能並高效運行。

GC堆限制

.NET Core減小了CoreCLR默認使用的內存,如G0代內存分配預算,以更好地與現代處理器緩存大小和緩存層次結構保持一致。

在新的建立的GC堆數量的策略裏,GC保留了一個內存片斷,每一個堆最小是16M,在低內存限制的機器上也能夠很好的運行。在多核CPU的機器上運行時,系統並無設置CPU的核數限制。例如,若是在48覈計算機上設置160 MB內存限制,則不須要建立48個GC堆。也就是說若是設置160 MB限制,則只會建立10個GC堆。若是未設置CPU限制,應用程序能夠利用計算機上的全部核心。

有了這樣的新策略,能夠不須要啓用Docker環境下的.NET Core應用的工做站GC的工做負載。

支持Docker內存限制

Docker資源限制創建在cgroup之上,而cgroup是Linux的內核功能。從運行時的角度來看,咱們須要定位cgroup原語。

設置cgroup限制時的.NET Core 3.0內存使用規則:

  • 默認GC堆大小:容器上cgroup內存限制的最大值20MB或最大值的75%
  • 每一個GC堆的最小保留段大小16MB,這將減小在具備大量內核和小內存限制的計算機上建立的堆數

爲了支持容器方案,添加了2個HardLimit配置:

  • GCHeapHardLimit - 指定GC堆的硬限制
  • GCHeapHardLimitPercent - 指定容許此進程使用的物理內存的百分比

若是同時指定了二者,則首先檢查GCHeapHardLimit,而且只有在未指定GCHeapHardLimit時才檢查GCHeapHardLimitPercent。

若是二者都未指定,但進程正在有內存限制的容器中運行,則默認是使用以下設置:

max(20mb,容器內存限制的75%)

若是指定了hardlimit配置,而且程序在有內存限制的容器中使用,GC堆的使用不會超過hardlimit限制,但總內存仍然受容器的內存限制。因此當咱們統計內存消耗時,基於容器內存限制得出的數據。

舉例:

進程在設置了200MB限制的容器中運行,用戶還將GCHeapHardLimit配置爲100MB。

若是把GC限制中100MB限制中的50MB用於GC,而容器限制中剩餘的100MB用於其餘用途,那麼內存消耗即爲(50+100)/200=75%。

GC將更積極地執行資源回收與釋放,由於GC堆越接近GCHeapHardLimit限制,就越能實現提供更多可用內存的目標,也越能使得應用程序能夠繼續而又安全地運行。若是算法計算出的結果認爲此時的GC效率低下,那麼將避免持續執行徹底阻塞的GC。

即便GC堆徹底壓縮,GC依然會拋出一個OutOfMemoryException異常出來,這是由於所分配的堆大小超過了GCHeapHardLimit的限制。

因而可知,.NET Core 3.0的設計是要穩定運行於有資源限制的容器中。

支持DockerCPU限制

在CPU限制的狀況下,Docker上設置的值將向上舍入爲下一個整數值。此值是CoreCLR使用的最大有效CPU核數。

默認狀況下,ASP.NET Core應用程序啓用了服務器GC(它不適用於控制檯應用程序),由於它能夠實現高吞吐量並減小跨核心的爭用。當進程僅限於單個處理器時,運行時會自動切換到工做站GC。即便您明確指定使用服務器GC,工做站GC也將始終用於單核環境。

經過計算CPU繁忙時間,設置CPU限制,咱們避免了線程池的各類推導性競爭:

  • 嘗試分配更多的線程以增長CPU繁忙時間
  • 嘗試分配更少的線程,由於添加更多的線程不會提升吞吐量

參考資料:

https://devblogs.microsoft.com/dotnet/using-net-and-docker-together-dockercon-2019-update/

https://github.com/dotnet/designs/blob/master/accepted/support-for-memory-limits.md

http://www.javashuo.com/article/p-untiwtry-ea.html

https://blog.csdn.net/koudaidai/article/details/7794793

相關文章
相關標籤/搜索