C# 類型、對象、線程棧和托管堆在運行時的關系


我們將討論類型、對象、線程棧和托管堆在運行時的相互關系,假定有以下兩個類定義:

internal class Employee
    {
        public int GetYearsEmployed()
        {
            return 1;
        }
        public virtual string GetProgressReport()
        {
            return "zhengwei";
        }
        public static Employee Lookup(string name)
        {
            Employee e = new Manager();
            return e;
        }
    }


    internal sealed class Manager : Employee
    {
        public override string GetProgressReport()
        {
            return "overrid zhengwei";
        }
    }

     Windows 進程已啟動,CLR已加載到其中,托管堆已初始化,而且已創建一個線程(連同它的1MB棧空間)。線程已執行了一些代碼,馬上就要調用M3方法。圖1展示了目前的狀態。Main方法包含的代碼演示了CLR是如何工作的。

JIT編譯器將Main的IL代碼轉換成本機的CPU指令,會注意到Main內部引用的所有類型,包括Employee,Int32,Manager以及String。 這時CLR要確認定義了這些類型的所有程序集都已加載。然后,利用程序集的元數據,CLR提取與這些類型有關的信息,
創建一些數據結構來表示類型本身。圖2展示了為Employee和Manager類型對象使用的數據結構。由於線程在調用Main前已執行了一些代碼,所以Int32和string類型對象已經創建好了,圖中沒有顯示它們。
    現在我們討論一一這些類型對象,我們在堆上創建的對象都包含兩個額外的成員:類型對象指針和同步塊索引。圖2中Employee和Manager類型對象都有這兩個成員。定義類型時,可在類型內部定義靜態數據字段。為這些靜態數據字段提供支援的字節在類型對象自身中分配。每個類型對象最后都包含一個方法表。在方法表中。類型定義的每個方法都有對應的記錄項。由於Employee定義了3個方法,所以有3個記錄項。Manager類型只定義了1個方法,所以Manager的方法表只有1個記錄項。
    當CLR確認方法需要的所有類型對象都已創建,Main的代碼已經編譯之后,就允許線程執行Main的本機代碼。Main代碼執行時必須在線程棧中為局部變量分配內存,如圖3 所示CLR自動將所有局部變量初始化為NULL或0,然而,如果代碼試圖訪問尚未顯式初始化的局部變量,C#會報錯。
    然后,Main執行代碼構造一個Manager對象。這造成在托管堆創建Manager類型的一個實例,如圖4所示,可以看出,和所有對象一樣,Manager對象也有類型對象指針和同步塊索引。該對象還包含必要的字節來容納Manager類型定義的所有實例數據字段,以及容納由Manager的任何基類(本例就是Employee和Object)定義的所有實例字段。任何時候在堆上新建對象,CLR都自動初始化內部的“類型對象指針”成員來引用和對象對應的類型對象(本例就是Manager類型對象)。此外,在調用類型的構造器之前,CLR會先初始化同步塊索引,並將對象的所有實例字段設為null或0。new 操作符返回Manager對象的內存地址,該地址保存到變量e中(e在線程棧上)。
 
     Main的下一行代碼調用Employee的靜態方法Lookup。調用靜態方法時,CLR會定位與定義靜態方法的類型對應的類型對象。然后,JIT編譯器在類型對象的方法表中查找與初調用方法對應的記錄項,對方法進行JIT編譯,再調用JIT編譯好的代碼。本例假定Employee的Lookup方法要查詢數據庫來查找joe,再假定數據庫指出的joe是公司的一名經理,所以在內部,Lookup方法在堆上構造一個新的Manager對象,用joe的信息初始化它,返回該對象的地址。該地址保存到局部變量e 中。這個操作的結果如圖5.
注意,e不再引用第一個Manager對象。事實上,由於沒有變量引用該對象,所以它是未來垃圾回收的主要目標。垃圾回收機制將自動回收該對象占用的內存。
 
     Main調用Employee的非虛實例方法GetYearsEmployed。調用非虛實例方法時,JIT編譯器會找到與“發出調用的那個變量(e)的類型(Employee)”對應的類型對象(Employee類型對象)。這時變量e初定義成一個Employee。如果Employee類型沒有定義正在調用的那個方法,JIT編譯器會回溯類層次結構(一直回溯至Object),並在沿途的每個類型中查找該方法。之所以能這樣回溯,是因為每個類型對象都有一個字段引用了它的基類型,這個信息在圖中沒有顯示。
     然后,JIT編譯器在類型對象的方法表中查找引用了被調用方法的記錄項,對方法進行JIT編譯,再調用JIT編譯好的代碼。本例假定Employee的GetYearsEmployed方法返回1。這個整數保存到局部變量year中。如圖6所示。
    Main調用Employee的虛實例方法GetProgressReport。調用虛實例方法時,JIT編譯器要在方法中生成一些額外的代碼。方法每次調用都會執行這些代碼。這些代碼首先檢查發出調用的變量,並跟隨地址來到發出調用的對象,然后,代碼檢查對象的內部的“類型對象批針成員”,該成員指向對象的實際類型。然后代碼在類型對象的方法表中查找引用了被調用方法的記錄項,對方法進行JIT編譯,再調用JIT編譯好的代碼。由於目前e引用一個Manager對象,所以會調用Manager的GetProgressReport實現。
    至此,我們已經討論了源代碼、IL和JIT編譯的代碼之間的關系。還討論了線程棧、實參、局部變量以及這些實參和變量如何引用托管堆上的對象。還知道對象含有一個指針指向對象的類型對象(類型對象中包含靜態字段和方法表)。還討論了JIT編譯器如何決定靜態方法、非虛實例方法以及實例方法的調用方式。理解這一切之后,可以深刻地認識CLR的工作方式。以后在建構、設計和實現類型、組件以及應用程序時,這些知識會帶來幫助。
    下面,我們將深入控計一下CLR內部發生的事情。
    注意Employee和Manager類型對象都包含“類型對象指針”成員。這是由於類型對象本質上也是對象。CLR創建類型對象時,必須初始化這些成員。初始化成什么呢?你肯定會這樣問。CLR開始在一個進程中運行時,會立即為MSCorLib.dll中定義的System.Type類型創建一個特殊的類型對象。Employee和Manager類型對象都是該類型的“實例”。因此,它們的類型對象指針成員會初始化成對System.Type類型對象的引用。如圖7所示
    當然,System.Type類型對象本身也是對象,內部也有“類型對象指針”成員。這個指針指向什么?它指向本身,因為System.Type類型對象本身是一個類型對象的“實例”。現在,我們總算理解了CLR的整個類型系統及其工作方式。
順便說一句,System.Object的GetType方法返回存儲指定對象的“類型對象指針”成員中的地址。也就是說,GetType方法返回指向對象的類型對象指針。這樣就可判斷系統中任何對象的真實類型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM