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