1. 值類型實例的創建位置:
對于值類型的實例,CLR在運行時有兩種分配方式:(1) 如果該值類型的實例作為類型中的方法(Method)中的局部變量,則該實例被創建在線程棧上;(2) 如果該值類型的實例作為類型的成員,則該實例作為引用類型(引用類型在GC堆或者LOH上創建)的實例的一部分,被創建在GC堆上。下面這段代碼演示了這兩種情況:
public class Test1
{
private int i;//上面(2)中的情況,生成Test的實例的同時,int類型的實例i被創建在GC堆上
public Test1()
{
byte b =0;//(1)中的情況,byte類型的實例b被創建在執行這段代碼的線程棧上
}
}
2. 引用類型實例的創建位置:
對于引用類型的實例,CLR在運行時也有兩種分配方式:(1) 如果該引用類型的實例的Size<85000Byte,則該實例被創建在GC(Garbage Collection)堆上(當CLR在分配和回收對象時,GC可能會對GC堆進行壓縮);(2) 如果該引用類型的實例的Size>=85000byte,則該實例被創建在LOH(Large Object Heap)上(LOH不會被壓縮)。面這段代碼演示了這兩種情況:
public class Test2
{
private int[] intArr;
public Test2()
{
private Object o = new Object();//引用o存在線程棧上,它指向GC堆上的Object實例
intArr = new int[21250];//符合(2)中的Size條件,int數組的實例被創建在LOH上
}
}
3. 托管對象被引用的七種途徑:
上面的代碼片段Test2中,也演示了引用托管對象的兩種途徑:
(1) intArr隨Test2實例創建的同時,被創建在GC堆上,由GC堆上的類型(Test2)實例持有托管對象(int數組的實例)的引用;
(2) 引用類型的變量o存在線程棧上,由線程棧上的局部變量(o)持有托管對象(Object實例)的引用。
其實還有以下五種途徑可以持有托管對象的引用:
(3) LOH堆上的實例(原理同1);
(4) 在與非托管語句交互操作或者P/Invoke情況下的句柄表(Handle Table);
(5) 寄存器,例如執行實例方法時的this指針和方法參數(IL中的call、callvirl指令是將函數參數進行自右往左的壓棧處理通過棧實現傳遞參數;而fastcall指令則可以最多將兩個參數分別保存到ECX和EDX兩個寄存器中,通過寄存器來實現參數傳遞,以提高程序的性能;這里的方法參數是指后一種fastcall的情況);
(6) 擁有終結器(finalizer)方法的對象的終結器隊列;
(7) 所屬類型的HandleTable(對象創建時,該HandleTable將持有一個弱引用Weak Reference)。下圖演示了這幾種情況:
4. 托管對象的結構:
從上圖中,我們可以看到,托管對象的引用并不是指向對象的起始位置,而是相對起始位置有+4Byte(DWord)的偏移量,這4個Byte稱為對象頭。下面轉載一段對該對象頭的介紹:對象頭保存一個間接指向SyncTableEntry表的索引(從1開始計數的syncblk編號)。SyncTableEntry維護一個反向的弱引用,以便CLR可以跟蹤SyncBlock的所有權。弱引用讓GC可以在沒有其它強引用存在時回收對象。SyncTableEntry還保存了一個指向SyncBlock的指針,包含了很少需要被一個對象的所有實例使用的有用的信息。這些信息包括對象鎖,哈希編碼,任何轉換層(thunking)數據和應用程序域的索引。對于大多數的對象實例,不會為實際的SyncBlock分配內存,而且syncblk編號為0。這一點在執行線程遇到如lock(obj)或者obj.GetHashCode的語句時會發生變化,如下所示:
Object obj = new Object();2
lock(obj) { /* Do some synchronized work here */ }3
obj.GetHashCode();在以上代碼中,smallObj會使用0作為它的起始的syncblk編號。lock語句使得CLR創建一個syncblk入口并使用相應的數值更新對象頭。因為C#的lock關鍵字會擴展為try-finally語句并使用Monitor類,一個用作同步的Monitor對象在syncblk上創建。堆GetHashCode的調用會使用對象的哈希編碼增加syncblk。在SyncBlock中有其它的域,它們在COM交互操作和封送委托(marshaling delegates)到非托管代碼時使用,不過這和典型的對象用處無關。
緊跟在syncblk編號后的是一個TypeHandle句柄(占4個byte),有點像C++中的虛方法表指針,但實際上這個TypeHandle指向的MethodTable比C++中的虛方法表復雜得多(這里先不介紹,以后有機會再介紹)。
在TypeHandle之后才開始存放實例字段的實例。默認情況下(即相當于在類上運用特性StructLayoutAttribute(LayoutKind.Auto)),實例字段會以內存最有效使用的方式排列,以便更有效地使用內存,排序之后字段在內存中的布局順序跟字段在類中聲明的順序不一定相同,這樣也給我們計算托管對象的Size帶來了不便....
<<未完待續>>下一篇將繼續討論如何獲得托管對象的Size。



浙公網安備 33010602011771號