.Net运行时的相互关系
閱讀目錄
- 前言
- 線程堆棧的分配
- 托管堆上對象的分配
- 結(jié)束語
?
前言
.Net中的運行時,以及各個類型、對象、線程堆棧以及托管堆之間的關(guān)系,在初學者(俺是初學者中的菜鳥 J)看來,有很多是難以理解的東西,俺在看了CLR Via C# 的前幾章后,現(xiàn)在將文中的大概意思并加以自己的理解,重現(xiàn)運行時,以及各個關(guān)系。希望各位盡量拍磚,多多指出不正確的地方,共同進步。
?線程堆棧的分配
?
???????? 圖1中展示了CLR加載的一個Microsoft Windows進程。在一個進程中,可能會存在多個線程。在創(chuàng)建一個線程時,這個線程會分配到一個1MB大小的堆棧。這個堆棧空間的作用:用于向方法傳遞 實參,并用于存儲在方法內(nèi)部定義的局部變量。圖1展示了一個線程的堆棧(右側(cè))。堆棧都是從高位內(nèi)存向地位內(nèi)存地址構(gòu)建。在左側(cè)圖中,該線程執(zhí)行了一些代 碼,它的堆棧上已經(jīng)有一些數(shù)據(jù)(右圖上半部分灰色區(qū)域)?,F(xiàn)在假定線程要執(zhí)行M1方法。?
???????? 在一個方法中,應(yīng)該包含一些開場白代碼,負責在方法開始前對變量進行初始化操作,以及一些收場白代碼,負責方法執(zhí)行完畢之后進行清理工作,以便返回調(diào)用者。當M1方法開始執(zhí)行時,它的開場白代碼在線程的對戰(zhàn)中為局部變量name分配內(nèi)存,如圖2所示:?
接 著,M1中的代碼執(zhí)行,調(diào)用M2方法,將局部變量name作為一個實參來傳遞。這造成name局部變量中的地址被壓入堆棧。在M2方法內(nèi)部,將使用名為s 的形參變量來標識堆棧位置(注意,有的架構(gòu)通過寄存器來傳遞實參以提升性能,但這對于當前的討論來說并不重要)。另外,在調(diào)用一個方法時,還會將一個“返 回地址”壓入堆棧。以便被調(diào)用的方法在完成之后,應(yīng)該返回到這個位置。參見圖3:?
???????? M2方法開始執(zhí)行時,他的開場白代碼在線程的堆棧中為局部變量length 和tally分配內(nèi)存,如圖4。然后,開始執(zhí)行M2方法內(nèi)部的代碼。最終,M2會執(zhí)行到return語句,這時CPU執(zhí)行指針會被設(shè)置成堆棧中剛才存儲的[返回地址] ,而 且M2的堆棧幀會進行輾轉(zhuǎn)開解(unwind)(個人大概理解意思是:釋放M2的內(nèi)部局部變量),然后堆棧內(nèi)部會恢復(fù)到圖2狀態(tài),之后,M1將繼續(xù)執(zhí)行后 面代碼,最終M1也會返回到它的調(diào)用者,這個過程其實跟M2是一樣的,M1執(zhí)行完成之后,M1的堆棧幀會進行輾轉(zhuǎn)開解,恢復(fù)成圖1所示那樣。跟著會執(zhí)行 M1后續(xù)的代碼。圖4:?
托管堆上對象的分配
???????? 討論完了堆棧上的內(nèi)存分配之后,我們再來看下托管堆上對象的分配。我們知道在.Net中值類型是存儲在堆棧上,引用類型是存儲在托管堆上,上面線程堆棧的分配中,name是string類型,屬于引用類型,string的分配屬于比較特殊的部分,這里我推薦:
Artech的大作:字符串的駐留(String Interning)
Anytao的大作:[你必須知道的.Net]第九回:品味類型—值類型與引用類型(中)—規(guī)則無邊
說明:在濤哥的這篇文章中,建議多看看精彩的評論。
???????? 現(xiàn)在,假定有以下兩個類定義如下:
// Employee 類定義internal class Employee{
public int GetYearEmployed(){…}
public virtual String GetProgressReport(){…}
public static Employee Lookup(String name)(){…}
}
// Manager類定義 繼承自 Employee
internal sealed class Manager:Employee{
public override String GetProgressReport(){…}
}
?? ? ? ? 現(xiàn)在Windows進程已經(jīng)啟動,CLR已經(jīng)加載完成,托管對已初始化,而且已經(jīng)創(chuàng)建好了一個線程連同他的1MB的堆??丶T摼€程已經(jīng)執(zhí)行了一些代碼, 現(xiàn)在馬上就要調(diào)用M3代碼。圖5展示了當前的狀況。M3方法包含的代碼演示了CLR是如何工作的。我們平時不會寫這樣的代碼,因為它們實際上并不做任何有 用的事情。圖5:?
???????? 當JIT編譯器將M3的IL代碼轉(zhuǎn)換成本地CPU指令時,會注意到M3內(nèi)部引用的所有類型:Employee,Int32,Manager 以及String(因為有“Joe”) 。這時,CLR要確保定義了這些類型的所有程序集已經(jīng)加載到AppDomain中。然后,利用程序集的元數(shù)據(jù),CLR提取有關(guān)這些類型的信息,并創(chuàng)建一些 數(shù)據(jù)結(jié)構(gòu)來表示類型本身。圖6展示了用于Employee 和Manager類型對象的數(shù)據(jù)結(jié)構(gòu)。由于線程之前已經(jīng)執(zhí)行了一些代碼,所以不妨假設(shè)int和String的類型對象已經(jīng)創(chuàng)建好了,所以圖中沒有顯示它 們。圖6:?
現(xiàn)在我們來討論下這些類型對象。在創(chuàng)建對象的時候,所有對象除了包含實例成員外,都會再包含兩個額外的成員:類型對象指針和同步塊索引。 從上圖中可以看出,Employee和Manager類型對象都有這兩個成員。定義一個類型時,可以在類型的內(nèi)部定義靜態(tài)數(shù)據(jù)字段。為這些靜態(tài)數(shù)據(jù)字段提 供支援的字節(jié)是在類型對象自身中分配的。在每個類型對象中,最后都包含一個方法表。在方法表中,類型中定義的每個方法都有一個對應(yīng)的紀錄項。
Employee 定義了三個方法,所以在它的方法表中有三個紀錄項。同理,Manager只定義了一個方法,所以在Manager的方法表中只有一個紀錄項。
???????? 現(xiàn)在,當CLR確定方法需要的所有類型對象都已創(chuàng)建,而且M3的代碼已經(jīng)編譯后,就允許線程開始執(zhí)行M3的本地代碼。M3的開場白代碼執(zhí)行時,必須從線程 的堆棧中為局部變量分配內(nèi)存(引用類型存儲引用,引用指向?qū)ο笏谕泄芏训钠频刂?#xff0c;此時尚未在托管堆創(chuàng)建對象,所以會賦值為null,在用new新增對 象后,才會指向新對象的引用地址,值類型存儲變量本身,),在圖中代碼中,CLR會自動將所有局部變量初始化為null或0。圖7:?
???????? 然后M3繼續(xù)執(zhí)行代碼,緊接著構(gòu)造一個Manager對象,這個構(gòu)造操作會在托管堆中創(chuàng)建Manager類型的一個實例,可以看出,和所有對象一 樣,Manager對象也有一個類型對象指針和同步塊索引。該對象還包含容納Manager類型定義的所有實例數(shù)據(jù)字段及其任何基類定義的所有實例字段所 需的字節(jié)。任何時候在堆上新建一個對象,CLR都會自動初始化內(nèi)部類型對象指針成員,讓它引用與對象對應(yīng)的類型對象(本例就是Manager類型對象)。 此外,CLR還會首先初始化同步塊索引,并將對象的所有實例字段設(shè)為null或0,然后才會調(diào)用類型構(gòu)造器(可能會修改某些實力字段),隨后 new操作符會返回新建的Manager對象的內(nèi)存地址,該地址保存在變量e中(e在線程的堆棧上)。圖8:?
緊 接著M3的下一行代碼調(diào)用Employee的靜態(tài)方法Lookup。在調(diào)用一個靜態(tài)方法時,CLR會定位與定義靜態(tài)方法的類型對應(yīng)的類型對象。然 后,CLR在類型對象的方法表中定位引用了被調(diào)用方法的紀錄項,然后對方法進行JIT編譯(如果需要的話),最后調(diào)用JIT編譯過的代碼。就本例來說,我 們假定Employee的Lookup方法要查詢一個數(shù)據(jù)庫來查找Joe。這里Lookup是返回一個Employee類型的對象。假定數(shù)據(jù)庫指出Joe 是公司的一名經(jīng)理,所以在內(nèi)部,Lookup方法在堆上構(gòu)造一個新的Manager對象,初始化它,然后返回該對象的地址,這個地址保存到局部變量e中。 操作的結(jié)果如圖9所示:?
???????? 注意,此時,e不再引用創(chuàng)建的第一個Manager對象。事實上,由于沒有變量引用這個對象,所以它是將來進行垃圾收集時的主要候選對象。
???????? M3的下一行代碼調(diào)用Employee的非虛實例方法GetYearEmployed。在調(diào)用一個非虛實例方法時,CLR會找到與發(fā)出調(diào)用的變量的的類型 對應(yīng)的類型對象。在本例中,e被定義成一個Employee(假如Employee類型沒有定義這個方法,則會回溯類層次結(jié)構(gòu),在基類查找)。然 后,CLR在類型對象的方法表中找到引用了被調(diào)用方法的紀錄項,對方法進行JIT編譯,然后調(diào)用JIT編譯過的代碼,就本例來說,假定該方法返回5,這個 整數(shù)保存在year中。結(jié)果如圖10:?
???????? M3的下一行代碼調(diào)用Employee的虛實例方法GetProgressReport。在調(diào)用一個虛實例方法時,CLR要做一些額外的工作。首先,它在 發(fā)出調(diào)用的變量中查找,然后跟隨地址到發(fā)出調(diào)用的對象。在本例中,變量e指向Joe 的Manager對象。然后,CLR會檢查對象的內(nèi)部類型對象指針成員,這個成員引用了對象的實際類型。然后,CLR在類型對象的方法表中定位引用了被調(diào) 用方法的紀錄項,對方法進行JIT編譯,然后調(diào)用編譯后的代碼,就本例來說,會調(diào)用Manager的GetProgressReport實現(xiàn),因為e引用 的是一個Manager對象,操作結(jié)果如圖11:?
???????? 注意,如果Employee的Lookup方法發(fā)現(xiàn)Joe只是一個Employee,而不是一個Manager,Lookup會在內(nèi)部構(gòu)造一個 Employee對象,它的類型對象指針將引用Employee類型對象。這樣一類,最終執(zhí)行的就是Emplouee的 GetProgressReport實現(xiàn),而不是Manager的GetProgressReport實現(xiàn)。
???????? 至此,已經(jīng)討論了源代碼、IL和JIT編譯的代碼之間的關(guān)系,還討論了線程的對戰(zhàn)、參數(shù)、局部變量、以及如何引用托管堆上的對象。我們還知道對象中包含一個指針,它指向?qū)ο蟮念愋蛯ο?#xff08;類型對象中包含靜態(tài)字段、方法表、類型對象指針和同步塊索引)。還討論了CLR如何調(diào)用靜態(tài)方法、非虛方法以及虛實例方法。理解這些后,可以深刻認識到CLR的工作方式。這些知識會帶來很大的幫助。
???????? 接下來再更深層一點,看看CLR內(nèi)部發(fā)生的事情。從前面幾個圖中,我們可以看到Employee和Manager創(chuàng)建類型對象時,必須初始化這些成員。那 么,具體初始化什么呢?CLR開始在一個進程中運行時,它會立即為System.Type類型創(chuàng)建一個特殊的類型對象。Employee和Manager 類型對象是該類型的實例。因此,他們的類型對象指針會初始化成Type類型對象的引用。如圖12:?
???????? 當然,System.Type類型對象本身也是一個對象,所以內(nèi)部也同樣包含一個類型對象指針成員。那么這個指針指向誰呢?它指向它本身。因為System.Type類型對象本身是一個類型對象的實例。?
結(jié)束語
???????? 現(xiàn)在,我們總算理解了CLR的整個類型對象及其工作方式,System.Object的GetType方法返回的是存儲在指定對象的“類型對象指針”,這樣,就可以判斷系統(tǒng)中任何對象(包括類型對象)的真實類型。
轉(zhuǎn)載于:https://www.cnblogs.com/zzunstu/p/3408949.html
總結(jié)
以上是生活随笔為你收集整理的.Net运行时的相互关系的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转帖:关于MongoDB你需要知道的几件
- 下一篇: main函数的参数问题 (转载)