天天看點

Java虛拟機深入研究

Java技術與Java虛拟機

Java虛拟機深入研究

圖1 Java四個方面的關系

運作期環境代表着Java平台,開發人員編寫Java代碼(.java檔案),然後将之編譯成位元組碼(.class檔案)。最後位元組碼被裝入記憶體,一旦位元組碼進入虛拟機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。從上圖也可以看出Java平台由Java虛拟機和Java應用程式接口搭建,Java語言則是進入這個平台的通道,用Java語言編寫并編譯的程式可以運作在這個平台上。這個平台的結構如下圖所示:

<a href="http://www.qqread.com/ArtImage/20060214/pp5_2.gif" target="_blank"></a>

Java虛拟機深入研究

那麼到底什麼是Java虛拟機(JVM)呢?通常我們談論JVM時,我們的意思可能是:

對JVM規範的的比較抽象的說明;

對JVM的具體實作;

在程式運作期間所生成的一個JVM執行個體。

對JVM規範的的抽象說明是一些概念的集合,它們已經在書《The Java Virtual Machine Specification》(《Java虛拟機規範》)中被詳細地描述了;對JVM的具體實作要麼是軟體,要麼是軟體和硬體的組合,它已經被許多生産廠商所實作,并存在于多種平台之上;運作Java程式的任務由JVM的運作期執行個體單個承擔。在本文中我們所讨論的Java虛拟機(JVM)主要針對第三種情況而言。它可以被看成一個想象中的機器,在實際的計算機上通過軟體模拟來實作,有自己想象中的硬體,如處理器、堆棧、寄存器等,還有自己相應的指令系統。

JVM在它的生存周期中有一個明确的任務,那就是運作Java程式,是以當Java程式啟動的時候,就産生JVM的一個執行個體;當程式運作結束的時候,該執行個體也跟着消失了。下面我們從JVM的體系結構和它的運作過程這兩個方面來對它進行比較深入的研究。

Java虛拟機的體系結構

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

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

Java虛拟機深入研究

圖3 JVM的體系結構

JVM的每個執行個體都有一個它自己的方法域和一個堆,運作于JVM内的所有的線程都共享這些區域;當虛拟機裝載類檔案的時候,它解析其中的二進制資料所包含的類資訊,并把它們放到方法域中;當程式運作的時候,JVM把程式初始化的所有對象置于堆上;而每個線程建立的時候,都會擁有自己的程式計數器和Java棧,其中程式計數器中的值指向下一條即将被執行的指令,線程的Java棧則存儲為該線程調用Java方法的狀态;本地方法調用的狀态被存儲在本地方法棧,該方法棧依賴于具體的實作。

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

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

虛拟機的内層循環的執行過程如下:

do{

取一個操作符位元組;

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

}while(程式未結束)

由于指令系統的簡單性,使得虛拟機執行的過程十分簡單,進而有利于提高執行的效率。指令中操作數的數量和大小是由操作符決定的。如果操作數比一個位元組大,那麼它存儲的順序是高位位元組優先。例如,一個16位的參數存放時占用兩個位元組,其值為:

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

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

對于本地方法接口,實作JVM并不要求一定要有它的支援,甚至可以完全沒有。Sun公司實作Java本地接口(JNI)是出于可移植性的考慮,當然我們也可以設計出其它的本地接口來代替Sun公司的JNI。但是這些設計與實作是比較複雜的事情,需要確定垃圾回收器不會将那些正在被本地方法調用的對象釋放掉。

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

Java虛拟機的寄存器用于儲存機器的運作狀态,與微處理器中的某些專用寄存器類似。Java虛拟機的寄存器有四種:

pc: Java程式計數器;

optop: 指向操作數棧頂端的指針;

frame: 指向目前執行方法的執行環境的指針;。

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),用于對運作時資料區進行操作時是不考慮類型的。

Java虛拟機的運作過程

上面對虛拟機的各個部分進行了比較詳細的說明,下面通過一個具體的例子來分析它的運作過程。

虛拟機通過調用某個指定類的方法main啟動,傳遞給main一個字元串數組參數,使指定的類被裝載,同時連結該類所使用的其它的類型,并且初始化它們。例如對于程式:

class HelloApp

{

public static void main(String[] args)

System.out.println("Hello World!");

for (int i = 0; i &lt; args.length; i++ )

System.out.println(args[i]);

}

編譯後在指令行模式下鍵入: java HelloApp run virtual machine

将通過調用HelloApp的方法main來啟動java虛拟機,傳遞給main一個包含三個字元串"run"、"virtual"、"machine"的數組。現在我們略述虛拟機在執行HelloApp時可能采取的步驟。

<a href="http://www.qqread.com/ArtImage/20060214/pp5_4.gif" target="_blank"></a>

Java虛拟機深入研究

圖4:虛拟機的運作過程

結束語

本文通過對JVM的體系結構的深入研究以及一個Java程式執行時虛拟機的運作過程的詳細分析,意在剖析清楚Java虛拟機的機理。