JVM體系結(jié)構(gòu)
時間:2022-02-19 05:09:01 | 來源:信息時代
時間:2022-02-19 05:09:01 來源:信息時代
JVM可以由不同的廠商來實現(xiàn)。由于廠商的不同必然導(dǎo)致JVM在實現(xiàn)上的一些不同,然而JVM還是可以實現(xiàn)跨平臺的特性,這就要歸功于設(shè)計JVM時的體系結(jié)構(gòu)了。我們知道,一個JVM實例的行為不光是它自己的事,還涉及到它的子系統(tǒng)、存儲區(qū)域、數(shù)據(jù)類型和指令這些部分,它們描述了JVM的一個抽象的內(nèi)部體系結(jié)構(gòu),其目的不光規(guī)定實現(xiàn)JVM時它內(nèi)部的體系結(jié)構(gòu),更重要的是提供了一種方式,用于嚴(yán)格定義實現(xiàn)時的外部行為。每個JVM都有兩種機(jī)制,一個是裝載具有合適名稱的類(類或是接口),叫做類裝載子系統(tǒng);另外的一個負(fù)責(zé)執(zhí)行包含在已裝載的類或接口中的指令,叫做運(yùn)行引擎。每個JVM又包括方法區(qū)、堆、Java棧、程序計數(shù)器和本地方法棧這五個部分。
JVM的每個實例都有一個它自己的方法域和一個堆,運(yùn)行于JVM內(nèi)的所有的線程都共享這些區(qū)域;當(dāng)虛擬機(jī)裝載類文件的時候,它解析其中的二進(jìn)制數(shù)據(jù)所包含的類信息,并把它們放到方法域中;當(dāng)程序運(yùn)行的時候,JVM把程序初始化的所有對象置于堆上;而每個線程創(chuàng)建的時候,都會擁有自己的程序計數(shù)器和Java棧,其中程序計數(shù)器中的值指向下一條即將被執(zhí)行的指令,線程的Java棧則存儲為該線程調(diào)用Java方法的狀態(tài);本地方法調(diào)用的狀態(tài)被存儲在本地方法棧,該方法棧依賴于具體的實現(xiàn)。
下面分別對這幾個部分進(jìn)行說明。
執(zhí)行引擎處于JVM的核心位置,在Java虛擬機(jī)規(guī)范中,它的行為是由指令集所決定的。盡管對于每條指令,規(guī)范很詳細(xì)地說明了當(dāng)JVM執(zhí)行字節(jié)碼遇到指令時,它的實現(xiàn)應(yīng)該做什么,但對于怎么做卻言之甚少。Java虛擬機(jī)支持大約248個字節(jié)碼。每個字節(jié)碼執(zhí)行一種基本的CPU運(yùn)算,例如,把一個整數(shù)加到寄存器,子程序轉(zhuǎn)移等。Java指令集相當(dāng)于Java程序的匯編語言。Java指令集中的指令包含一個單字節(jié)的操作符,用于指定要執(zhí)行的操作,還有0個或多個操作數(shù),提供操作所需的參數(shù)或數(shù)據(jù)。許多指令沒有操作數(shù),僅由一個單字節(jié)的操作符構(gòu)成。
虛擬機(jī)的內(nèi)層循環(huán)的執(zhí)行過程如下:
do{
取一個操作符字節(jié);
根據(jù)操作符的值執(zhí)行一個動作;
}while(程序未結(jié)束)
由于指令系統(tǒng)的簡單性,使得虛擬機(jī)執(zhí)行的過程十分簡單,從而有利于提高執(zhí)行的效率。指令中操作數(shù)的數(shù)量和大小是由操作符決定的。如果操作數(shù)比一個字節(jié)大,那么它存儲的順序是高位字節(jié)優(yōu)先。例如,一個16位的參數(shù)存放時占用兩個字節(jié),其值為:
第一個字節(jié)*256 第二個字節(jié)字節(jié)碼。
指令流一般只是字節(jié)對齊的。指令tableswitch和lookup是例外,在這兩條指令內(nèi)部要求強(qiáng)制的4字節(jié)邊界對齊。
對于本地方法接口,實現(xiàn)JVM并不要求一定要有它的支持,甚至可以完全沒有。Sun公司實現(xiàn)Java本地接口(JNI)是出于可移植性的考慮,當(dāng)然我們也可以設(shè)計出其它的本地接口來代替Sun公司的JNI。但是這些設(shè)計與實現(xiàn)是比較復(fù)雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調(diào)用的對象釋放掉。
Java的堆是一個運(yùn)行時數(shù)據(jù)區(qū),類的實例(對象)從中分配空間,它的管理是由垃圾回收來負(fù)責(zé)的:不給程序員顯式釋放對象的能力。Java不規(guī)定具體使用的垃圾回收算法,可以根據(jù)系統(tǒng)的需求使用各種各樣的算法。
Java方法區(qū)與傳統(tǒng)語言中的編譯后代碼或是Unix進(jìn)程中的正文段類似。它保存方法代碼(編譯后的java代碼)和符號表。在當(dāng)前的Java實現(xiàn)中,方法代碼不包括在垃圾回收堆中,但計劃在將來的版本中實現(xiàn)。每個類文件包含了一個Java類或一個Java界面的編譯后的代碼。可以說類文件是Java語言的執(zhí)行代碼文件。為了保證類文件的平臺無關(guān)性,Java虛擬機(jī)規(guī)范中對類文件的格式也作了詳細(xì)的說明。其具體細(xì)節(jié)請參考Sun公司的Java虛擬機(jī)規(guī)范。
Java虛擬機(jī)的寄存器用于保存機(jī)器的運(yùn)行狀態(tài),與微處理器中的某些專用寄存器類似。Java虛擬機(jī)的寄存器有四種:
pc:Java程序計數(shù)器;
optop:指向操作數(shù)棧頂端的指針;
frame:指向當(dāng)前執(zhí)行方法的執(zhí)行環(huán)境的指針;。
vars:指向當(dāng)前執(zhí)行方法的局部變量區(qū)第一個變量的指針。
在上述體系結(jié)構(gòu)圖中,我們所說的是第一種,即程序計數(shù)器,每個線程一旦被創(chuàng)建就擁有了自己的程序計數(shù)器。當(dāng)線程執(zhí)行Java方法的時候,它包含該線程正在被執(zhí)行的指令的地址。但是若線程執(zhí)行的是一個本地的方法,那么程序計數(shù)器的值就不會被定義。
Java虛擬機(jī)的棧有三個區(qū)域:局部變量區(qū)、運(yùn)行環(huán)境區(qū)、操作數(shù)區(qū)。
局部變量區(qū)
每個Java方法使用一個固定大小的局部變量集。它們按照與vars寄存器的字偏移量來尋址。局部變量都是32位的。長整數(shù)和雙精度浮點數(shù)占據(jù)了兩個局部變量的空間,卻按照第一個局部變量的索引來尋址。(例如,一個具有索引n的局部變量,如果是一個雙精度浮點數(shù),那么它實際占據(jù)了索引n和n 1所代表的存儲空間)虛擬機(jī)規(guī)范并不要求在局部變量中的64位的值是64位對齊的。虛擬機(jī)提供了把局部變量中的值裝載到操作數(shù)棧的指令,也提供了把操作數(shù)棧中的值寫入局部變量的指令。
運(yùn)行環(huán)境區(qū)
在運(yùn)行環(huán)境中包含的信息用于動態(tài)鏈接,正常的方法返回以及異常捕捉。
動態(tài)鏈接
運(yùn)行環(huán)境包括對指向當(dāng)前類和當(dāng)前方法的解釋器符號表的指針,用于支持方法代碼的動態(tài)鏈接。方法的class文件代碼在引用要調(diào)用的方法和要訪問的變量時使用符號。動態(tài)鏈接把符號形式的方法調(diào)用翻譯成實際方法調(diào)用,裝載必要的類以解釋還沒有定義的符號,并把變量訪問翻譯成與這些變量運(yùn)行時的存儲結(jié)構(gòu)相應(yīng)的偏移地址。動態(tài)鏈接方法和變量使得方法中使用的其它類的變化不會影響到本程序的代碼。
正常的方法返回
如果當(dāng)前方法正常地結(jié)束了,在執(zhí)行了一條具有正確類型的返回指令時,調(diào)用的方法會得到一個返回值。執(zhí)行環(huán)境在正常返回的情況下用于恢復(fù)調(diào)用者的寄存器,并把調(diào)用者的程序計數(shù)器增加一個恰當(dāng)?shù)臄?shù)值,以跳過已執(zhí)行過的方法調(diào)用指令,然后在調(diào)用者的執(zhí)行環(huán)境中繼續(xù)執(zhí)行下去。
異常捕捉
異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程序中的原因是:①動態(tài)鏈接錯,如無法找到所需的class文件。運(yùn)行時錯,如對一個空指針的引用。程序使用了throw語句。
當(dāng)異常發(fā)生時,Java虛擬機(jī)采取如下措施:
§檢查與當(dāng)前方法相聯(lián)系的catch子句表。每個catch子句包含其有效指令范圍,能夠處理的異常類型,以及處理異常的代碼塊地址。
§與異常相匹配的catch子句應(yīng)該符合下面的條件:造成異常的指令在其指令范圍之內(nèi),發(fā)生的異常類型是其能處理的異常類型的子類型。如果找到了匹配的catch子句,那么系統(tǒng)轉(zhuǎn)移到指定的異常處理塊處執(zhí)行;如果沒有找到異常處理塊,重復(fù)尋找匹配的catch子句的過程,直到當(dāng)前方法的所有嵌套的catch子句都被檢查過。
§由于虛擬機(jī)從第一個匹配的catch子句處繼續(xù)執(zhí)行,所以catch子句表中的順序是很重要的。因為Java代碼是結(jié)構(gòu)化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程序計數(shù)器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程序計數(shù)器值下發(fā)生的異常情況。
§如果找不到匹配的catch子句,那么當(dāng)前方法得到一個'未截獲異常'的結(jié)果并返回到當(dāng)前方法的調(diào)用者,好像異常剛剛在其調(diào)用者中發(fā)生一樣。如果在調(diào)用者中仍然沒有找到相應(yīng)的異常處理塊,那么這種錯誤將被傳播下去。如果錯誤被傳播到最頂層,那么系統(tǒng)將調(diào)用一個缺省的異常處理塊。
操作數(shù)棧區(qū)
機(jī)器指令只從操作數(shù)棧中取操作數(shù),對它們進(jìn)行操作,并把結(jié)果返回到棧中。選擇棧結(jié)構(gòu)的原因是:在只有少量寄存器或非通用寄存器的機(jī)器(如Intel486)上,也能夠高效地模擬虛擬機(jī)的行為。操作數(shù)棧是32位的。它用于給方法傳遞參數(shù),并從方法接收結(jié)果,也用于支持操作的參數(shù),并保存操作的結(jié)果。例如,iadd指令將兩個整數(shù)相加。相加的兩個整數(shù)應(yīng)該是操作數(shù)棧頂?shù)膬蓚€字。這兩個字是由先前的指令壓進(jìn)堆棧的。這兩個整數(shù)將從堆棧彈出、相加,并把結(jié)果壓回到操作數(shù)棧中。
每個原始數(shù)據(jù)類型都有專門的指令對它們進(jìn)行必須的操作。每個操作數(shù)在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。操作數(shù)只能被適用于其類型的操作符所操作。例如,壓入兩個int類型的數(shù),如果把它們當(dāng)作是一個long類型的數(shù)則是非法的。在Sun的虛擬機(jī)實現(xiàn)中,這個限制由字節(jié)碼驗證器強(qiáng)制實行。但是,有少數(shù)操作(操作符dupe和swap),用于對運(yùn)行時數(shù)據(jù)區(qū)進(jìn)行操作時是不考慮類型的。
本地方法棧,當(dāng)一個線程調(diào)用本地方法時,它就不再受到虛擬機(jī)關(guān)于結(jié)構(gòu)和安全限制方面的約束,它既可以訪問虛擬機(jī)的運(yùn)行期數(shù)據(jù)區(qū),也可以使用本地處理器以及任何類型的棧。例如,本地棧是一個C語言的棧,那么當(dāng)C程序調(diào)用C函數(shù)時,函數(shù)的參數(shù)以某種順序被壓入棧,結(jié)果則返回給調(diào)用函數(shù)。在實現(xiàn)Java虛擬機(jī)時,本地方法接口使用的是C語言的模型棧,那么它的本地方法棧的調(diào)度與使用則完全與C語言的棧相同。
關(guān)鍵詞:結(jié)構(gòu),體系