類和對象的CLR內存佈局

   c#中有許多的內置類型,int是最常用的數據類型之一。它代表一個4個字節的有符號整數。對於下面的這代碼卻可以通過c#編譯器的語法檢查。

int i = int.Parse("123");
string num = 100.ToString();

生成的IL指令如下:

仔細查看可發現call指令調用的是System.Int32的方法。爲什麼一個四字節的int型整數會與一個複雜的System.Int32結構類型掛上關係呢?答案其實並不複雜。

CLR爲每種內置的數據類型準備了一張“方法表”,其中列出了此類型所有的方法。如下圖是所示。在程序運行時,CLR會查表完成調用過程。

上圖內存佈局圖,其中代表整數值的內存單元與System.Int32類型方法表之間的邏輯關係不是在程序運行時建立的,而是在編譯時確定的。對於用戶自定義的類型,道理是類似的,如c#中的結構,在程序運行時,同樣存在一張對應的方法表供CLR調用。與System.Int32這種內置數據類型不一樣,用戶自定義類型的相關信息存放在用戶程序集元數據中,CLR在裝入用戶程序集時根據這些元數據信息創建相應的方法表。創建好之後用法和內置類型的用法沒什麼兩樣。

引用類型變量的內存佈局

引用類型變量用於引用一個生存於託管堆中的對象,其作用類似於一個類型安全的對象指針。通過引用類型變量可以設置它所應用對象的字段或屬性和調用它的方法。CLR是如何做到這一點?

與值類型情況相似,每種引用類型也有一個方法表。下圖爲引用類型變量的內存佈局圖。

如圖所示,每個引用類型都有一個方法表,此方法表中包含了類中定義的所有靜態字段和靜態、實例方法。而使用new關鍵字創建的實例對象中存放有此類的實例字段。同時擁有一個指針指向此類所對應的方法表。

一個類可以創建多個對象,這些實例對象共享同一個方法表。

上圖中引用類型變量1和2分別引用兩個對象,這兩個對象都屬於引用類型1,所以它們共享同一個方法表

引用類型變量3和4引用同一個對象,此對象屬於引用類型2,它有一個指針指向引用類型2的方法表。

特別注意

類所定義的實例字段與方法是分離的,不管是靜態方法還是動態方法,都統一放置在方法表中,但是字段分兩個地方存放,靜態字段存放在方法表中,實力字段存放在對象中。

這個對象模型很好的解釋了面向對象中“類的靜態字段被此類創建的所有對象共享”這一現象。

另外要注意一點,引用變量生存於線程堆棧中,而對象生存於託管堆中。

每個對象還有一個“同步塊索引”,它其實是CLR維護的一個“同步塊表”的索引,在多線程運行環境下,CLR可用這些同步塊來同步多個線程對此對象的訪問。

也正是由於類型方法表的存在,c#不允許一個引用變量像c++的指針那樣隨意轉換類型,從而避免了對引用變量的誤用,也限制了其所能進行的操作。正因如此,我們才說引用變量是一種“類型安全”的對象指針。

方法的JIT編譯原理

弄清楚了值類型與引用類型的變量的內存佈局之後,就能明白CLR對方法代碼的動態編譯原理。示例代碼如下:

class MyClassExample
{
    public void f()
    {
    }
}
class Program
{
    static void Main(string[] args)
    {
        MyClassExample obj = new MyClassExample();
        obj.f();
    }
}

根據前面的介紹,我們知道CLR運行此程序時,會自動爲MyClassExample創建一個方法表,在此方法表中保存了MyClassExample類的所有靜態字段和方法,每個字段和方法在表中佔用一行。如下圖:

 

因爲MyClassExample類沒有定義靜態字段,所以方法表中沒有字段的對應行,但由於CLR規定所有的類型都必須是Object的子類,所以方法表中出現了Object類的四個虛方法,方法表的最下方的兩行是MyClassExample類的f方法和構造函數。

注意:

子類的方法表中只包含基類定義的虛方法,基類的其他方法不會出現在子類的方法表中。

當程序第一次運行時,CLR負責從程序集元數據中提取MyClassExample類型信息,創建MyClassExample方法表,每個方法表項都對應一個“方法樁”(Method Stub),其內容爲調用CLR中JIT編譯器的call指令。

當某個方法被第一次調用時,CLR查類型方法表找到對應的方法表項,取出其方法樁,發現其中是一條調用JIT編譯器的call指令,CLR先從程序集中提取出此方法所對應的IL指令代碼,將其傳送給JIT編譯器,由JIT編譯器將這些IL指令代碼即時編譯成可以在當前CPU上執行的本地代碼,即時編譯工作完成後,CLR將此方法的本地代碼緩存起來,並將此方法對應之方法樁內容由原先的調用JIT編譯器的call指令改爲無條件跳轉的jmp指令,跳轉地址就是JIT編譯器編譯完成的方法本地代碼首地址。修改完方法樁後,CLR執行方法的本地代碼。

當第二次調用方法時,由於它的本地代碼已緩存,而且對應的方法樁也修改爲一條跳轉指令jmp,CLR就可以直接執行本地代碼,不再需要JIT編譯器重新編譯了。

由於CLR緩存了方法的本地代碼,所以對方法的第二次調用比第一次要快得多。這就是CLR的即時編譯原理

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章