.NET中 類型,對象,線程棧,托管堆 在運行時的關系
The Relationship at Run Time between Types,Objects,A Thread's Stack,and The Managed Heap for .NET
by 唐小崇
http://www.cnblogs.com/tangchong
.NET中的類型,無論是值類型或引用類型都是繼承自Object的類。這點跟Java類似,但與C/C++有很大不同。既然值類型與引用類型都是類,那它們的沒有什么不同的地方。而最值得關注的不同就是:值類型對象的值直接存儲在線程棧中,引用類型對象的值存放在托管堆中,它的引用存放在線程棧中。本篇博文就談談CLR的線程棧,托管堆策略。
當我們執行一個.NET程序, CLR 會加載一個新的進程,這個進程可能會(按照程序的編寫需求)包含多個線程。當一個線程被創建,它會申請一個 1MB大小棧(stack)。這個棧是以高位到地位保存的。線程棧是用來保存本地變量,參數,返回地址的,稍后會有詳細解釋。同時CLR會創建一個托管堆。(由於內容太多,先從線程棧開始講起,稍后再講托管堆)
現在,讓我們通過兩個例子方法M1,M2來了解線程棧與類型對象的關系:
我們先看看線程棧的樣子。圖1為一個線程棧的示例,我們假設該程序已經執行了一部分代碼,並即將執行M1方法。
圖1:線程棧示例
線程棧有以下作用:
·保存向某個方法傳遞的參數(passing arguments);
·保存當前執行的方法內的本地變量(local variales);
·保存當前執行方法的返回地址(return address);
所有的方法都會包含一些 序幕代碼(prologue code)和 收場代碼(epilogue code) ,序幕代碼用來初始化一個方法,這些代碼把本地變量,參數,返回地址壓入線程棧中。而收場代碼會在函數執行完畢后會清理給該函數分配的空間,並將指令指針(CPU's instruction pointer)指向返回地址(即指向該函數的調用者),從而釋放線程棧空間。
當我們開始執行M1,CLR將 本地變量name、向M2傳遞的參數s 和 返回地址 壓入線程棧中,如圖2所示:
圖2:將本地變量name、向M2傳遞的參數s和返回地址壓入線程棧中
接着我們執行到M2(name);CLR開始調用M2方法,這時序幕代碼會在線程棧里分配兩個本地變量 length,tally,如圖3所示。當M2方法執行完畢后,收場代碼清理M2分配的空間,並將指令指針指向返回地址(即M1)。此時,我們的線程棧又回到圖2所示的狀態。
圖3:M2方法開始執行時線程棧的情況
以上,就是整個線程棧的執行情況。
我們接着來看看CLR中的托管堆。說到托管堆,就不得不提引用類型。下面我們列兩個示例類,Employee和它的子類Manager:
internal class Employee { public Int32 GetYearsEmployed() { ... } public virtual String GetProgressReport() { ... } public static Employee Lookup(String name) { ... } } internal sealed class Manager : Employee { public override String GetProgressReport() { ... }
如圖4所示:我們現在加入一個新方法M3。當我們的程序開始執行,CLR 會初始化線程棧和托管堆。
首先JIT編譯器將M3方法的中間代碼(IL)JIT編譯為本地指令(native CPU instructions)。
然后CLR檢測M3引用的所有類型(本例中為Employee,Int32,Manager,String),這時CLR會確保提供這些類型的程序集已經被加載(否則,會報錯)。
最后CLR提取有關這些類型的信息,並創建一些數據結構(Type Object)來表示的類型本身。也就是說,CLR會先創建 Type Object,然后通過它來創建類的實例對象。
圖4:當M3方法被調用。CLR在托管堆中創建對應的類的類型對象(Type Object)
在創建好的類型對象 Type Object(並不是類的實例對象)中包括以下成員
類型對象指針(Type object ptr)
同步塊索引(Sync block index)
靜態成員(Statoc fieds)
方法列表(method table)
緊接着,當CLR確認M3所需求的所有的類型對象(Type Object)都被創建,則開始執行M3的本地代碼。如圖5所示,序幕代碼在線程棧中創建本地變量並將它們初始化為null或者0。
圖5 序幕代碼在線程棧中創建本地變量
程序開始執行e = new Manager();
如圖6所示,這行代碼指示CLR在托管堆中創建一個Manager類型的實例對象(instance)。該實例對象包含: 類型對象指針、 同步塊索引以及 該類型中的定義成員(包括它的父類成員,在本例中父類為Employee,Object)。
然后,CLR會自動將該實例對象(Manager Object)的 類型對象指針(Type object ptr)指向相應的類型對象(Manager Type Object),此外,CLR會初始化同步塊索引和所有成員為0並調用構造器(構造函數)。
最后new 操作符將創建好的實例對象在托管堆中的地址 返回並賦值給線程棧中的引用類型變量e。
圖6:創建並初始化一個Manager實例對象
下面我們看看3種不同的方法:靜態方法,非虛方法,虛方法的執行情況。
下一行代碼是e = Employee.Lookup("Joe"); 這里調用了Employee的靜態方法Lookup()。
如圖7所示,當調用一個靜態方法,首先JIT編譯器會定位到該方法對應的類型Type Object對象(本例中為Employee Type Object)。然后將該Type Object函數列表中的對應方法(本例中是Lookup)JIT編譯,並執行。
我們假設Joe存在並且是一個經理,則Lookup函數會在托管堆中創建一個Manager實例對象,並用Joe初始化它。最后將這個Manager實例對象的地址返回,賦值給e。
圖7:靜態函數Lookup被調用,創建並用joe初始化一個Manager 對象,並賦值給e
繼續執行下一行代碼:year = e.GetYearsEmployed();
如圖8所示:當我們調用一個非虛函數(nonvirtual instance method),JIT編譯器會定位到 調用者的類型(e的類型為Employee)對應Type Object中(本例中為Employee Type Object)。在該Type Object中的方法列表中,查找對應方法(如果沒找到,會向其父類尋找直到Object為止)。JIT編譯該方法,並執行。我們不妨假設joe已經工作5年了。則Employee 的 GetYearsEmployed方法返回5,並賦值給線程棧中的year變量。
圖8:調用Employee的非虛函數 GetYearsEmployed。
接下來一行代碼是e.GetProgressReport();該方法是一個虛方法(virtual instance method)。
調用一個虛方法前,JIT編譯器會額外的執行一些代碼。這些代碼會先查找到 調用該方法的變量(e)所指向的實例對象(在本例中,即為用Joe初始化的Manager實例對象)。接下來,檢查該對象中的類型對象指針,以找到對象的真正類型type object(本例中為Manager Type Object)。最后JIT編譯該type object方法列表中的對應方法,並執行。
如圖9所示,JIT編譯並執行的是 Manager Type Object中的GetProgressReprot方法,而不是Employee Type Object中的。
圖9:調用虛函數GetProgerssReport(),實際類型Manager的type Object中的方法被執行
至此,示例代碼結束。我們討論了調用靜態方法,非虛函數,虛函數的三種情況。但我們還有一點沒有完成。
我們會注意到在Type Object中也有Type object ptr,這是因為這些Type Object也是一個“類型”的實例對象。它們的類型比較特殊,叫做System.Type,定義在MSCorLib.dll中。當CLR開始執行一個進程,它會先為System.Type創建一個Type Oject,稱為Type Type Object。如圖10所示,本例的 Employee Type Object,Manager Type Object都是Type Type Object的“實例對象”。最后,Type Type Object 本身也是一個對象,它的Type object ptr 指向自身。這就是.NET 萬物皆對象的思想。
圖10:Emloyee和Manager的type objects 是 System.Type 類型的實例對象