歡迎光臨
每天分享高質量文章

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

記憶體分配概要

前段時間在園子裡看到有人提到了GC學習的重要性,很贊同他的觀點。充分瞭解GC可以幫助我們更好的認識.NET的設計以及為何在雲原生開發中.NET Core會佔有更大的優勢,這也是一個程式員成長到更高層次所需要經歷的過程。在認識GC的過程中,我們先看一下.NET中記憶體分配的概要知識。
.NET分配記憶體,主要依據託管資源和非託管資源進行分配。託管資源分配到了託管堆中並受CLR的管理,非託管資源分配到了非託管堆中。該節主要討論託管資源的分配。
CLR支援兩種基本型別:值型別和取用型別。CLR對這兩種型別在執行時有兩種分配方式:

記憶體的分配過程如下圖所示

需要註意的是,CLR還要維護一個指標,稱為NextObjPtr,這個指標指向下一個物件再堆中的分配位置。初始化時,NextObjPtr設為地址空間區域的基地址。一個區域被非垃圾物件填滿後,CLR會分配更多的區域,指標也會不斷偏移。new運運算元會傳回物件的取用,就在傳回這個取用之前,NextObjPtr指標的值會加上物件佔用的位元組數來得到一個新值,即下一個物件放入托管堆時的地址。

垃圾回收演演算法與GC執行機制

常用的垃圾回收演演算法主要有取用計數演演算法和取用跟蹤演演算法。取用計算有著明顯的缺陷,.NET使用的垃圾回收演演算法是取用跟蹤法。小記:關於垃圾回收演演算法,我記得有一個知識點,在C#中如果出現了迴圈取用是否會導致記憶體上限溢位?如果比較瞭解這兩種演演算法就會知道不會上限溢位。

GC Root

取用跟蹤演演算法,透過一系列GCRoot物件作為起始點,從這些點開始向下搜尋,搜尋的路徑成為取用鏈,當一個物件到GC沒有任何取用鏈,說明物件可以被回收。

GC Root可以類比樹來解釋

GC根節點存在於堆疊中,指向Teacher取用物件。它包含一個ArrayList訂單集合,由Teacher物件取用。集合本身也包含對其元素的取用,隨著搜尋深度的增加,樹也不斷長大。

GC根節點的取用源來自

(1)、堆疊
(2)、全域性或靜態變數
(3)、CPU暫存器
(4)、互操作取用(COM / API呼叫中使用的.NET物件)
(5)、物件終結取用(objects finalization references)

GC執行機制

GC引入了代的概念,分為三種代,G0、G1、G2,G0物件生存週期較短,越往後生存週期越長(雖然G2中由於直接儲存了大物件,又由於G2不是每次都會掃描,所以大多數情況下,G2中的物件的生存週期比G0中的更長)。

GC執行如下圖所示

需要註意的是,CLR想要進行垃圾回收時,會立即掛起執行託管程式碼中的所有執行緒,正在執行非託管程式碼的執行緒不會掛起。所以再多執行緒環境下,可能會出現莫名其妙的詭異問題。

下圖為GC的整體執行流程,包含五個步驟:

垃圾回收時機與樣式

CLR會在一下情況發生時,執行GC操作

      1、當GC的代的預算大小已經達到閾值而無法對新物件分配空間的時候,比如GC的第0代已滿;

  2、顯式呼叫System.GC.Collect()(顯示呼叫要慎重,因為手動呼叫可能會與自動執行的GC衝突,從而導致無法預知的問題);

  3、其他特殊情況,比如,作業系統記憶體不足、CLR解除安裝AppDomain、CLR關閉,甚至某些極端情況下系統引數設定改變也可能導致GC回收。

關於GC樣式主要有

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

詳細資訊請參閱:https://www.cnblogs.com/dacc123/p/10980718.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繁忙時間
  • 嘗試分配更少的執行緒,因為新增更多的執行緒不會提高吞吐量

參考資料:

Using .NET and Docker Together – DockerCon 2019 Update

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

https://www.cnblogs.com/dacc123/p/10980718.html

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

贊(0)

分享創造快樂