.Net托管堆布局
加載堆 主要是供CLR內部使用,作為承載程序的元數據。
HighFrequencyHeap 存放CLR高頻使用的內部數據,比如MethodTable,MethodDesc. 通過is判斷類型之間的繼承關系,調用接口的方法和虛方法,都需要訪問MethodTable
LowFrequencyHeap 存放CLR低頻使用的內部數據,比如EEClass,ClassLoader. GC信息與異常處理表,它們都只在發生時才訪問,因此訪問頻率不高。
StringLiteralMap 字符串駐留池:https://www.cnblogs.com/lmy5215006/p/18494483 字符串對象本身存儲在FOH堆中,String Literal Map只是一個索引
StubHeap 函數入口的代碼堆 CodeHeap JIT編譯代碼使用的內部堆,比如生成IL。 VirtualCallStubHeap 虛方法調用的內部堆 使用!eeheap -loader可以查看
眼見為實
新版sos呈現方式不一樣,可以使用老版sos展示文中所述內容
托管堆 大家的老朋友了,不做過多解釋,由GC統一管理的內存堆.一個.NET程序中所有的Domain都會共用一個托管堆
SOH 略略略 LOH 略略略 POH 固定對象專屬的堆,比如非托管線程訪問托管對象,就需要把對象固定起來,避免被GC回收造成非托管代碼的訪問違例 . 使用!eeheap -gc可以查看
眼見為實
凍結堆 .NET8推出來的一個新堆,用來存放永遠不會被GC管理的永生對象,比如string 字面量。 簡單來說,就是一個對象你都永遠不會釋放了,還放在托管堆就是浪費了。不如單獨拎出來存。
眼見為實
https://www.cnblogs.com/lmy5215006/p/18515971
段 上述所說的各種堆,只是一個邏輯上的概念。作為內存的物理承載。由堆段(Heap Seg-ment)實現. 簡單來說,段是托管堆的物理表示。
眼見為實
segment begin allocated committed allocated size committed size 段指針的對象地址 內存分配的起始點 內存分配的末尾點 已提交的分配大小 已提交的大小
SOH小對象堆 堆只是一個抽象的概念,在物理上的表現形式為內存段,作為CLR細化堆的一種管理單位。多個段組成了堆。
.NET8之前的段結構 在.NET 8 之前,段分為SOH,LOH,POH 三個段。 對于SOH段有點特殊,因為段上面還有分代邏輯。包含0代和1代的對象只會分配在新分配的內存段上(臨時段),剩下的每個段都是2代的段 可以看到,代只是一個邏輯概念,并沒有獨立的段空間。0,1,2代共享段空間。
.NET8的段結構 到了.NET 8,代已經不是一個邏輯概念,而是一個物理概念。 每個代都有了自己獨立的段空間。
代機制 每當GC觸發時,所有對象(非固定)都會進行升代,直到gen2為止。
obj對象剛創建,為0代 內存地址為0x00000263ee009528,0x01fb08000028>0x000001fb080b71e0>01fb080b9068 說明obj放在0代里 第一次GC,obj升為1代 內存地址在1代空間范圍內 第二次GC,obj升為2代 內存地址在2代空間范圍內 代邊界 細心的朋友會發現一個盲點,就是obj剛剛創建的時候,0代內存起始點為0263ee000028,升為1代后,1代內存起始點也變為了0263ee000028,2代也同樣。 這就引申出另一個概念,GC升代,不是簡單的copy對象從0代到1代。而是移動代的邊界。 每次GC觸發時,代邊界指針會在多個“地址段”上遷移,通過這種邏輯操作,達到性能的最高,可以觀察上面的 Allocated 區,一會給了 0gen,一會又給了 1gen,一會又給了 2gen
LOH大對象堆 大對象堆存儲所有>=85000byte的對象,但也是有例外。LOH堆上對象管理相對寬松,沒有“代”機制,默認情況下也不會壓縮。
例外1-32位環境下的double[] static void Main (string [] args )
{
double [] array1 = new double [999 ];
Console.WriteLine(GC.GetGeneration(array1));
double [] array2 = new double [1000 ];
Console.WriteLine(GC.GetGeneration(array2));
double [,] array3 = new double [32 ,32 ];
Console.WriteLine(GC.GetGeneration(array3));
long [] array4 = new long [1000 ];
Console.WriteLine(GC.GetGeneration(array4));
Debugger.Break();
Console.ReadKey();
}
這里有個很奇怪的現象,在32位環境 下,array2的大小= 4b+4+4+1000*8=8012byte. 遠遠<=85000byte. 為什么被分配到了LOH堆? 這主要跟內存對齊有關,double的未對齊訪問非常昂貴,遠遠超過long,ulong,int。這對于64位環境來說不是問題,總是對SOH與LOH使用8byte對齊。但對于4字節對齊的32位環境。這就是個大問題了. 所以CLR開發團隊決定將閾值大于1000的double放入LOH堆(LOH堆總是8byte對齊)。避免了double未對齊訪問的巨大成本
例外2-StringInter與靜態成員以及元數據 https://www.cnblogs.com/lmy5215006/p/18515971 參考此文,在.NET5之前沒有POH堆,所以CLR內部使用的三個數組也會進入LOH堆。 三個數組分別為
static對象的object[] 字符串池 object[] 元數據 RuntimeType object[] 其實很好理解,這些都是低頻變化的內容,放在LOH堆上好過放在SOH堆。
POH堆 POH堆解決了什么問題? 從.NET5開始,CLR團隊給pinned的對象單獨放入一個段中,這樣pinned對象不會和普通對象混在一起。導致大量細小Free空間。從而降低托管堆碎片化,也降低了代降級的頻次。
有點遺憾的是,非托管代碼造成的對象固定,并不會移動到POH堆中。因此代降級的現象依舊存在。 感覺未來微軟可以重點優化這塊,固定對象是GC速度最大的阻礙。
如何使用POH堆? 在.NET 8中,將對象放入POH堆是一種“有意為之” 行為,必須調用 GC 類提供的 AllocateArray 和 AllocateUninitializedArray 方法并設置 pinned=true
FOH FOH堆解決了什么問題? 在.NET8中,如果一個對象在創建的時候,就明確知道是“永生” 對象,那就沒必要納入托管堆的管理范圍,只會徒增GC的工作量。因此干脆把對象放在托管堆之外,來提高性能
常見的例子就是字符串的字面量(literal)
static對象布局,不會被GC回收的對象1 靜態的基元類型(short,int,long) ,它的值本身并不存放在托管堆上。而是存放在Domain中的高頻堆中
靜態的引用類型則不同。真正的對象存放在托管堆上,再由POH中一個object[]持有,最后被高頻堆中的m_pGCStatics所管理
Domain下每一個Module都維護了一個DomainLocalModule結構,靜態變量放在該Module中
眼見為實:靜態基元類型分配在高頻堆上? internal class Program
{
static long age = 10086 ;
static void Main (string [] args )
{
age = 12 ;
Console.WriteLine("done. " + age);
Debugger.Break();
}
}
通過匯編得知,static a的地址為00007ff9a618e4a8 觀察高頻堆地址可以發現,00007FF9A6180000<00007ff9a618e4a8 <00007FF9A6190000 。明顯屬于高頻堆
眼見為實:靜態引用類型分配在哪? internal class Program
{
public static Person person = new Person();
static void Main (string [] args )
{
var num = person.age;
Console.WriteLine(num);
Debugger.Break();
}
}
public class Person
{
public int age = 12 ;
}
使用!gcwhere命令來查看person對象屬于0代中,說明對象本身分配在托管堆
使用!gcroot命令查看它的引用根,發現它被一個object[]所持有
再查看object[]的所屬代,可以看到該對象屬于POH堆
bp coreclr!JIT_GetSharedNonGCStaticBase_Helper 下斷點來獲取 DomainLocalModule 實例 注意,這里我重新運行了一遍,所以object[]內存地址有變
字符串駐留池布局,不會被GC回收的對象2 關于字符串的不可變性,參考此文:https://www.cnblogs.com/lmy5215006/p/18494483
在.NET8之前,字符串駐留與靜態引用類型處理模式無差別。 .NET 8加入FOH堆之后,會將編譯期間就能確定的字符串放入FOH堆,以便提高GC性能。
眼見為實 static void Main (string [] args )
{
var str1 = "hello FOH" ;
var str2 = Console.ReadLine();
string .Intern(str2);
Console.WriteLine($"str1={str1} ,str2={str2} " );
Debugger.Break();
}
編譯期間能確定的,直接加入了FOH
運行期間確定,與靜態引用類型處理流程一致
轉自https://www.cnblogs.com/lmy5215006/p/18583743
該文章在 2024/12/14 10:51:40 編輯過