.Net CLR GC 動態載入短暫堆閾值的計算及閾值超量的計算

2022-07-26 18:00:12

楔子

今天你躺平了嗎?生活是如此的無趣,歡迎大家一起來躺平

前言:

很多書籍或者很多文章,對於CLR或者GC這塊只限於長篇大論的理論性概念,對於裡面的如何運作模式,卻幾乎一無所知。高達近百萬行的CPP檔案,畢竟讀懂的沒有幾個。以下取自CLR.Net 6 PreView版本

分配量超過閾值

GC觸發裡面有一個GC被觸發的條件是,分配的記憶體塊超過閾值。這個閾值是在Generation代裡面的static_data裡面的儲存的固定數值。當你分配的記憶體塊超過這個閾值的時候,就會觸發GC進行垃圾回收。來看看這個閾值動態載入和超過閾值觸發GC垃圾回收之後,重新計算閾值的演演算法。

初始化

在CLR啟動的時候,會初始化Generation的靜態資料,此時會填充閾值。
Generation的部分靜態資料結構:

struct static_data
{
    size_t min_size; 閾值的下限
    size_t max_size; 閾值的上限
    size_t fragmentation_limit; 碎片空間的上限
    float fragmentation_burden_limit; 碎片空間百分比的上限
    float limit; 限度的下限
    float max_limit; 限度的上限
    uint64_t time_clock; 
    size_t gc_clock; 
};

下面是GC定義的初始化引數數值,以上面的結構參考下面:

static static_data static_data_table[latency_level_last - latency_level_first + 1][total_generation_count] =
{
    {
        // gen0
        {0, 0, 40000, 0.5f, 9.0f, 20.0f, (1000 * 1000), 1},
        // gen1
        {160*1024, 0, 80000, 0.5f, 2.0f, 7.0f, (10 * 1000 * 1000), 10},
        // gen2
        {256*1024, SSIZE_T_MAX, 200000, 0.25f, 1.2f, 1.8f, (100 * 1000 * 1000), 100},
        // loh
        {3*1024*1024, SSIZE_T_MAX, 0, 0.0f, 1.25f, 4.5f, 0, 0},
        // poh
        {3*1024*1024, SSIZE_T_MAX, 0, 0.0f, 1.25f, 4.5f, 0, 0},
    }
}

直接套用:

0代: 閾值下限0,無限制,限度下限9,限度上限20
1代: 閾值下限160kb,無限制,限度下限2,限度上限7
2代:  閾值下限256kb,無限制,限度下限1.2,限度上限1.6

這是最初始化的值,什麼意思呢?簡而言之,就是CLR在載入此初始化的值的之後,進行了動態的分配的閾值的上下限。也就是說實際上GC的閾值和這裡面表明的靜態數值有差別。
下面計算均為函數init_static_data裡面
0代閾值的下限動態計算方式(以工作站模式為例):

1.通過gc的組態檔(gcconfig)來獲取組態檔裡面設定的值,如果獲取此值失敗則跳轉到第二步
2.通過windows API GetLogicalProcessorInformation獲取到你當前電腦處理器最大的快取值。
3.下面就是GC第0代閾值的演演算法了
0代閾值下限= = max((4*你當前電腦處理器最大快取值/5),(256*1024))
你當前電腦處理器最大快取值 =  max(你當前電腦處理器最大快取值, (256*1024))
while(如果0代閾值下限*處理器個數>你當前堆分配實體記憶體總大小/6)
{
  0代閾值下限 = 0代閾值下限/2
  if(0代閾值下限 <=你當前電腦處理器最大快取值 )
  {
    0代閾值下限 = 你當前電腦處理器最大快取值
  }
}
if( 0代閾值下限 > = 小物件堆段(SOH)/2)
{
   0代閾值下限 = 小物件堆段(SOH)/2
}
0代閾值下限 = 0代閾值下限 / 8 * 5;// 到了這裡才是最終第0代閾值的下限,而非上面generaton的靜態資料static_data_table裡面的下限閾值為0.

第0代閾值的上限呢?(此處同時看下伺服器模式和工作站模式)

伺服器模式0代閾值上限的演演算法:
0代閾值上限=max (6*1024*1024, min ( Align(soh_segment_size/2), 200*1024*1024))
工作站模式0代閾值上限的演演算法
0代閾值上限= max (0代閾值的下限,0代閾值的上限)
0代閾值上限 = Align (0代閾值的下限)

第1代閾值的上限(伺服器模式和工作站模式)

第1代閾值的下限是160kb,那麼上限呢?
伺服器模式1代閾值的上限=max (6*1024*1024, Align(小物件堆的大小/2));
工作站模式1代閾值的上限=gen1_max_size = Align (伺服器模式1代閾值的上限/0)

閾值上限的重新計算

當閾值上限的剩餘空間不足以容納當前CLR分配的記憶體塊的時候,就造成了GC。GC之後,就會重新計算這個閾值的上限。這裡是演演算法

if(GC開始前存活的物件為0)
{
   閾值的上限 = 當前代的閾值下限
}
else
{
   if(當前代 >= 第二代)
   {
     cst = min (1.0f, float (GC之後活著的物件大小) / float (GC之前活著物件大小));
    if(cst<((限度的上限-限度的下限)/(限度的下限*(限度的上限 - 1.0f)))    
	{
       限度= ((限度的下限 - 限度的下限* cst)/ (1.0f - (cst * 限度的下限)))
    }
	else
	  限度 = 限度的上限
	  
	size_t max_growth_size = (限度的上限 / 限度)
	if( 當前代的大小 > = max_growth_size)
	 新分配量= 閾值的上限
	 else
	 {
	   新分配量 = (size_t) min (max ( (限度 * 當前代的大小), gc的最小值), 限度的上限);
	   限度的上限 =  max((新分配量 - 當前代的大小),gc的最小值);
	 }
  }
   else //如果是第二代以內的GC
   {
     cst = GC後活著的物件大小/ GC前活著物件大小
	     if (cst < ((限度的上限 - 限度的下限) / (限度的下限 * (限度的上限-1.0f))))
        限度=  ((限度的下限 - 限度的上限*cst) / (1.0f - (cst * 限度的下限)));
    else
        限度= 限度的上限;
		
    閾值的上限=min (max ((f * (GC後活著物件的大小)), 限度的下限), 限度的上限);

   }
}

由於這個過程過於複雜,中間很多細節。但是大體都是這樣。

以上參考如下:
https://github.com/dotnet/runtime/tree/main/src/coreclr/gc
微信公眾號:jianghupt QQ群:676817308