華文網

Java虛擬機器的體系結構

剛才已經提到,JVM可以由不同的廠商來實現。由於廠商的不同必然導致JVM在實現上的一些不同,然而JVM還是可以實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。

我們知道,

一個JVM實例的行為不光是它自己的事,還涉及到它的子系統、存儲區域、資料類型和指令這些部分,它們描述了JVM的一個抽象的內部體系結構,其目的不光規定實現JVM時它內部的體系結構,更重要的是提供了一種方式,用於嚴格定義實現時的外部行為。每個JVM都有兩種機制,一個是裝載具有合適名稱的類(類或是介面),叫做類裝載子系統;另外的一個負責執行包含在已裝載的類或介面中的指令,
叫做運行引擎。每個JVM又包括方法區、堆、 Java棧、程式計數器和本地方法棧這五個部分,這幾個部分和類裝載機制與運行引擎機制一起組成的體系結構圖為:

圖1 JVM的體系結構

JVM的每個實例都有一個它自己的方法域和一個堆,運行於JVM內的所有的執行緒都共用這些區域;當虛擬機器裝載類檔的時候,

它解析其中的二進位資料所包含的類資訊,並把它們放到方法域中;當程式運行的時候,JVM把程式初始化的所有物件置於堆上;而每個執行緒創建的時候,都會擁有自己的程式計數器和 Java棧,其中程式計數器中的值指向下一條即將被執行的指令,執行緒的Java棧則存儲為該執行緒調用Java方法的狀態;本地方法調用的狀態被存儲在本地方法棧,
該方法棧依賴於具體的實現。

下面分別對這幾個部分進行說明。

執行引擎處於JVM的核心位置,在Java虛擬機器規範中,它的行為是由指令集所決定的。儘管對於每條指令,規範很詳細地說明了當JVM執行位元組碼遇到指令時,它的實現應該做什麼,但對於怎麼做卻言之甚少。Java虛擬機器支援大約248個位元組碼。每個位元組碼執行一種基本的CPU運算,例如,把一個整數加到寄存器,副程式轉移等。

Java指令集相當於Java程式的組合語言。

Java指令集中的指令包含一個單字節的操作符,用於指定要執行的操作,還有0個或多個運算元,提供操作所需的參數或資料。許多指令沒有運算元,僅由一個單字節的操作符構成。

虛擬機器的內層迴圈的執行過程如下:

do{

取一個操作符位元組;

根據操作符的值執行一個動作;

}while(程式未結束)

由於指令系統的簡單性,使得虛擬機器執行的過程十分簡單,從而有利於提高執行的效率。

指令中運算元的數量和大小是由操作符決定的。如果運算元比一個位元組大,那麼它存儲的順序是高位位元組優先。例如,一個16位的參數存放時佔用兩個位元組,其值為:

第一個位元組*256+第二個位元組位元組碼。

指令流一般只是位元組對齊的。指令tableswitch和lookup是例外,在這兩條指令內部要求強制的4位元組邊界對齊。

對於本地方法介面,實現JVM並不要求一定要有它的支持,甚至可以完全沒有。Sun公司實現Java本地介面(JNI)是出於可攜性的考慮,當然我們也可以設計出其它的本地介面來代替Sun公司的JNI。但是這些設計與實現是比較複雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調用的物件釋放掉。

Java的堆是一個運行時資料區,類的實例(物件)從中分配空間,它的管理是由垃圾回收來負責的:不給程式師顯式釋放物件的能力。Java不規定具體使用的垃圾回收演算法,可以根據系統的需求使用各種各樣的演算法。

Java方法區與傳統語言中的編譯後代碼或是Unix進程中的正文段類似。它保存方法代碼(編譯後的java代碼)和符號表。在當前的Java實現中,方法代碼不包括在垃圾回收堆中,但計畫在將來的版本中實現。每個類檔包含了一個Java類或一個Java介面的編譯後的代碼。可以說類檔是 Java語言的執行代碼檔。為了保證類檔的平臺無關性,Java虛擬機器規範中對類檔的格式也作了詳細的說明。其具體細節請參考Sun公司的Java 虛擬機器規範。

Java虛擬機器的寄存器用於保存機器的運行狀態,與微處理器中的某些專用寄存器類似。Java虛擬機器的寄存器有四種:

1.pc: Java程式計數器;

2.optop: 指向運算元棧頂端的指標;

3.frame: 指向當前執行方法的執行環境的指標;。

4.vars: 指向當前執行方法的區域變數區第一個變數的指標。

在上述體系結構圖中,我們所說的是第一種,即程式計數器,每個執行緒一旦被創建就擁有了自己的程式計數器。當執行緒執行Java方法的時候,它包含該執行緒正在被執行的指令的位址。但是若執行緒執行的是一個本地的方法,那麼程式計數器的值就不會被定義。

Java虛擬機器的棧有三個區域:區域變數區、運行環境區、運算元區。

區域變數區

每個Java方法使用一個固定大小的區域變數集。它們按照與vars寄存器的字偏移量來定址。區域變數都是32位的。長整數和雙精度浮點數佔據了兩個區域變數的空間,卻按照第一個區域變數的索引來定址。(例如,一個具有索引n的區域變數,如果是一個雙精度浮點數,那麼它實際佔據了索引n和n+1所代表的存儲空間)虛擬機器規範並不要求在區域變數中的64位的值是64位對齊的。虛擬機器提供了把區域變數中的值裝載到運算元棧的指令,也提供了把運算元棧中的值寫入區域變數的指令。

運行環境區

在運行環境中包含的資訊用於動態連結,正常的方法返回以及異常捕捉。

動態連結

運行環境包括對指向當前類和當前方法的解譯器符號表的指標,用於支援方法代碼的動態連結。方法的class檔代碼在引用要調用的方法和要訪問的變數時使用符號。動態連結把符號形式的方法調用翻譯成實際方法調用,裝載必要的類以解釋還沒有定義的符號,並把變數訪問翻譯成與這些變數運行時的存儲結構相應的偏移位址。動態連結方法和變數使得方法中使用的其它類的變化不會影響到本程式的代碼。

正常的方法返回

如果當前方法正常地結束了,在執行了一條具有正確類型的返回指令時,調用的方法會得到一個返回值。執行環境在正常返回的情況下用於恢復調用者的寄存器,並把調用者的程式計數器增加一個恰當的數值,以跳過已執行過的方法調用指令,然後在調用者的執行環境中繼續執行下去。

異常捕捉

異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程式中的原因是:①動態連結錯,如無法找到所需的class檔。②運行時錯,如對一個空指標的引用。程式使用了throw語句。

當異常發生時,Java虛擬機器採取如下措施:

·檢查與當前方法相聯繫的catch子句表。每個catch子句包含其有效指令範圍,能夠處理的異常類型,以及處理異常的代碼塊位址。

·與異常相匹配的catch子句應該符合下面的條件:造成異常的指令在其指令範圍之內,發生的異常類型是其能處理的異常類型的子類型。如果找到了匹配的catch子句,那麼系統轉移到指定的異常處理塊處執行;如果沒有找到異常處理塊,重複尋找匹配的catch子句的過程,直到當前方法的所有嵌套的 catch子句都被檢查過。

·由於虛擬機器從第一個匹配的catch子句處繼續執行,所以catch子句表中的順序是很重要的。因為Java代碼是結構化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程式計數器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程式計數器值下發生的異常情況。

·如果找不到匹配的catch子句,那麼當前方法得到一個"未截獲異常"的結果並返回到當前方法的調用者,好像異常剛剛在其調用者中發生一樣。如果在調用者中仍然沒有找到相應的異常處理塊,那麼這種錯誤將被傳播下去。如果錯誤被傳播到最頂層,那麼系統將調用一個缺省的異常處理塊。

運算元棧區

機器指令只從運算元棧中取運算元,對它們進行操作,並把結果返回到棧中。選擇棧結構的原因是:在只有少量寄存器或非通用寄存器的機器(如 Intel486)上,也能夠高效地模擬虛擬機器的行為。運算元棧是32位的。它用於給方法傳遞參數,並從方法接收結果,也用於支持操作的參數,並保存操作的結果。例如,iadd指令將兩個整數相加。相加的兩個整數應該是運算元棧頂的兩個字。這兩個字是由先前的指令壓進堆疊的。這兩個整數將從堆疊彈出、相加,並把結果壓回到運算元棧中。

每個原始資料類型都有專門的指令對它們進行必須的操作。每個運算元在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。運算元只能被適用於其類型的操作符所操作。例如,壓入兩個int類型的數,如果把它們當作是一個long類型的數則是非法的。在Sun的虛擬機器實現中,這個限制由位元組碼驗證器強制實行。但是,有少數操作(操作符dupe和swap),用於對運行時資料區進行操作時是不考慮類型的。

本地方法棧,當一個執行緒調用本地方法時,它就不再受到虛擬機關于結構和安全限制方面的約束,它既可以訪問虛擬機器的運行期資料區,也可以使用本地處理器以及任何類型的棧。例如,本地棧是一個C語言的棧,那麼當C程式調用C函數時,函數的參數以某種順序被壓入棧,結果則返回給調用函數。在實現Java虛擬機器時,本地方法介面使用的是C語言的模型棧,那麼它的本地方法棧的調度與使用則完全與C語言的棧相同。

對於本地方法介面,實現JVM並不要求一定要有它的支持,甚至可以完全沒有。Sun公司實現Java本地介面(JNI)是出於可攜性的考慮,當然我們也可以設計出其它的本地介面來代替Sun公司的JNI。但是這些設計與實現是比較複雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調用的物件釋放掉。

Java的堆是一個運行時資料區,類的實例(物件)從中分配空間,它的管理是由垃圾回收來負責的:不給程式師顯式釋放物件的能力。Java不規定具體使用的垃圾回收演算法,可以根據系統的需求使用各種各樣的演算法。

Java方法區與傳統語言中的編譯後代碼或是Unix進程中的正文段類似。它保存方法代碼(編譯後的java代碼)和符號表。在當前的Java實現中,方法代碼不包括在垃圾回收堆中,但計畫在將來的版本中實現。每個類檔包含了一個Java類或一個Java介面的編譯後的代碼。可以說類檔是 Java語言的執行代碼檔。為了保證類檔的平臺無關性,Java虛擬機器規範中對類檔的格式也作了詳細的說明。其具體細節請參考Sun公司的Java 虛擬機器規範。

Java虛擬機器的寄存器用於保存機器的運行狀態,與微處理器中的某些專用寄存器類似。Java虛擬機器的寄存器有四種:

1.pc: Java程式計數器;

2.optop: 指向運算元棧頂端的指標;

3.frame: 指向當前執行方法的執行環境的指標;。

4.vars: 指向當前執行方法的區域變數區第一個變數的指標。

在上述體系結構圖中,我們所說的是第一種,即程式計數器,每個執行緒一旦被創建就擁有了自己的程式計數器。當執行緒執行Java方法的時候,它包含該執行緒正在被執行的指令的位址。但是若執行緒執行的是一個本地的方法,那麼程式計數器的值就不會被定義。

Java虛擬機器的棧有三個區域:區域變數區、運行環境區、運算元區。

區域變數區

每個Java方法使用一個固定大小的區域變數集。它們按照與vars寄存器的字偏移量來定址。區域變數都是32位的。長整數和雙精度浮點數佔據了兩個區域變數的空間,卻按照第一個區域變數的索引來定址。(例如,一個具有索引n的區域變數,如果是一個雙精度浮點數,那麼它實際佔據了索引n和n+1所代表的存儲空間)虛擬機器規範並不要求在區域變數中的64位的值是64位對齊的。虛擬機器提供了把區域變數中的值裝載到運算元棧的指令,也提供了把運算元棧中的值寫入區域變數的指令。

運行環境區

在運行環境中包含的資訊用於動態連結,正常的方法返回以及異常捕捉。

動態連結

運行環境包括對指向當前類和當前方法的解譯器符號表的指標,用於支援方法代碼的動態連結。方法的class檔代碼在引用要調用的方法和要訪問的變數時使用符號。動態連結把符號形式的方法調用翻譯成實際方法調用,裝載必要的類以解釋還沒有定義的符號,並把變數訪問翻譯成與這些變數運行時的存儲結構相應的偏移位址。動態連結方法和變數使得方法中使用的其它類的變化不會影響到本程式的代碼。

正常的方法返回

如果當前方法正常地結束了,在執行了一條具有正確類型的返回指令時,調用的方法會得到一個返回值。執行環境在正常返回的情況下用於恢復調用者的寄存器,並把調用者的程式計數器增加一個恰當的數值,以跳過已執行過的方法調用指令,然後在調用者的執行環境中繼續執行下去。

異常捕捉

異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程式中的原因是:①動態連結錯,如無法找到所需的class檔。②運行時錯,如對一個空指標的引用。程式使用了throw語句。

當異常發生時,Java虛擬機器採取如下措施:

·檢查與當前方法相聯繫的catch子句表。每個catch子句包含其有效指令範圍,能夠處理的異常類型,以及處理異常的代碼塊位址。

·與異常相匹配的catch子句應該符合下面的條件:造成異常的指令在其指令範圍之內,發生的異常類型是其能處理的異常類型的子類型。如果找到了匹配的catch子句,那麼系統轉移到指定的異常處理塊處執行;如果沒有找到異常處理塊,重複尋找匹配的catch子句的過程,直到當前方法的所有嵌套的 catch子句都被檢查過。

·由於虛擬機器從第一個匹配的catch子句處繼續執行,所以catch子句表中的順序是很重要的。因為Java代碼是結構化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程式計數器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程式計數器值下發生的異常情況。

·如果找不到匹配的catch子句,那麼當前方法得到一個"未截獲異常"的結果並返回到當前方法的調用者,好像異常剛剛在其調用者中發生一樣。如果在調用者中仍然沒有找到相應的異常處理塊,那麼這種錯誤將被傳播下去。如果錯誤被傳播到最頂層,那麼系統將調用一個缺省的異常處理塊。

運算元棧區

機器指令只從運算元棧中取運算元,對它們進行操作,並把結果返回到棧中。選擇棧結構的原因是:在只有少量寄存器或非通用寄存器的機器(如 Intel486)上,也能夠高效地模擬虛擬機器的行為。運算元棧是32位的。它用於給方法傳遞參數,並從方法接收結果,也用於支持操作的參數,並保存操作的結果。例如,iadd指令將兩個整數相加。相加的兩個整數應該是運算元棧頂的兩個字。這兩個字是由先前的指令壓進堆疊的。這兩個整數將從堆疊彈出、相加,並把結果壓回到運算元棧中。

每個原始資料類型都有專門的指令對它們進行必須的操作。每個運算元在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。運算元只能被適用於其類型的操作符所操作。例如,壓入兩個int類型的數,如果把它們當作是一個long類型的數則是非法的。在Sun的虛擬機器實現中,這個限制由位元組碼驗證器強制實行。但是,有少數操作(操作符dupe和swap),用於對運行時資料區進行操作時是不考慮類型的。

本地方法棧,當一個執行緒調用本地方法時,它就不再受到虛擬機關于結構和安全限制方面的約束,它既可以訪問虛擬機器的運行期資料區,也可以使用本地處理器以及任何類型的棧。例如,本地棧是一個C語言的棧,那麼當C程式調用C函數時,函數的參數以某種順序被壓入棧,結果則返回給調用函數。在實現Java虛擬機器時,本地方法介面使用的是C語言的模型棧,那麼它的本地方法棧的調度與使用則完全與C語言的棧相同。