Java虛擬機

Java虛擬機(JVM)一種用於計算設備的規範,可用不同的方式(軟件或硬件)加以實現。編譯虛擬機的指令集與編譯微處理器的指令集非常類似。Java虛擬機包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。

      Java虛擬機(JVM)是可運行Java代碼的假想計算機。只要根據JVM規格描述將解釋器移植到特定的計算機上,就能保證經過編譯的任何Java代碼能夠在該系統上運行。

      Java虛擬機是一個想象中的機器,在實際的計算機上通過軟件模擬來實現。Java虛擬機有自己想象中的硬件,如處理器、堆棧、寄存器等,還具有相應的指令系統。

一、Java虛擬機(JVM)概述

      1. 爲什麼要使用Java虛擬機

      Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機是實現這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機後,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。

      2. 誰需要了解Java虛擬機

      Java虛擬機是Java語言底層實現的基礎,Java語言感興趣的人都應對Java虛擬機有個大概的瞭解。這有助於理解Java語言的一些性質,也有助於使用Java語言。對於要在特定平臺上實現Java虛擬機的軟件人員,Java語言的編譯器作者以及要用硬件芯片實現Java虛擬機的人來說,則必須深刻理解Java虛擬機的規範。另外,如果你想擴展Java語言,或是把其它語言編譯成Java語言的字節碼,你也需要深入地瞭解Java虛擬機。

      3. Java虛擬機支持的數據類型

      Java虛擬機支持Java語言的基本數據類型如下:

      byte: //1字節有符號整數的補碼

      short: //2字節有符號整數的補碼

      int: //4字節有符號整數的補碼

      long: //8字節有符號整數的補碼

      float: //4字節IEEE754單精度浮點數

      double: //8字節IEEE754雙精度浮點數

      char: //2字節無符號Unicode字符

      幾乎所有的Java類型檢查都是在編譯時完成的。上面列出的原始數據類型的數據在Java執行時不需要用硬件標記。操作這些原始數據類型數據的字節碼(指令)本身就已經指出了操作數的數據類型,例如iaddladd fadddadd指令都是把兩個數相加,其操作數類型別是intlongfloatdouble。虛擬機沒有給boolean(布爾)類型設置單獨的指令。boolean型的數據是由integer指令,包括integer返回來處理的。boolean型的數組則是用byte數組來處理的。虛擬機使用IEEE754格式的浮點數。不支持IEEE格式的較舊的計算機,在運行Java數值計算程序時,可能會非常慢。

      虛擬機支持的其它數據類型包括:

      object//對一個Javaobject(對象)4字節引用

      returnAddress//4字節,用於jsr/ret/jsr-w/ret-w指令

      :Java數組被當作object處理。

 

      虛擬機的規範對於object內部的結構沒有任何特殊的要求。在Sun公司的實現中,object的引用是一個句柄,其中包含一對指針:一個指針指向該 object的方法表,另一個指向該object的數據。用Java虛擬機的字節碼錶示的程序應該遵守類型規定。Java虛擬機的實現應拒絕執行違反了類型規定的字節碼程序。Java虛擬機由於字節碼定義的限制似乎只能運行於32位地址空間的機器上。但是可以創建一個Java虛擬機,它自動地把字節碼轉換成64位的形式。從Java虛擬機支持的數據類型可以看出,Java對數據類型的內部格式進行了嚴格規定,這樣使得各種Java虛擬機的實現對數據的解釋是相同的,從而保證了Java的與平臺無關性和可移植性。

 

二、Java虛擬機JVM規格描述

      JVM的設計目標是提供一個基於抽象規格描述的計算機模型,爲解釋程序開發人員提很好的靈活性,同時也確保Java代碼可在符合該規範的任何系統上運行。 JVM對其實現的某些方面給出了具體的定義,特別是對Java可執行代碼,即字節碼(Bytecode)的格式給出了明確的規格。這一規格包括操作碼和操作數的語法和數值、標識符的(數值)表示方式、以及Java類文件中的Java對象、常量緩衝池在JVM的存儲映象。這些定義爲JVM解釋器開發人員提供了所需的信息和開發環境。Java的設計者希望給開發人員以隨心所欲使用Java的自由。

JVM定義了控制Java代碼解釋執行和具體實現的五種規格,它們是:   

      *JVM指令系統

      *JVM寄存器

      *JVM棧結構   

      *JVM垃圾回收堆   

      *JVM存儲區

 

      2.1 JVM指令系統

      JVM指令系統同其他計算機的指令系統極其相似。Java指令也是由操作碼和操作數兩部分組成。操作碼爲8位二進制數,操作數緊隨在操作碼的後面,其長度根據需要而不同。操作碼用於指定一條指令操作的性質(在這裏我們採用彙編符號的形式進行說明),如iload表示從存儲器中裝入一個整數,anewarray表示爲一個新數組分配空間,iand表示兩個整數的"" ret用於流程控制,表示從對某一方法的調用中返回。當長度大於8位時,操作數被分爲兩個以上字節存放。JVM採用了"big endian"的編碼方式來處理這種情況,即高位bits存放在低字節地址中。這同 Motorola及其他的RISC CPU採用的編碼方式是一致的,而與Intel採用的"little endian "的編碼方式即低位bits存放在低位字節地址的方法不同。

      Java指令系統是以Java語言的實現爲目的設計的,其中包含了用於調用方法和監視多線程系統的指令。Java8位操作碼的長度使得JVM最多有256種指令,目前已使用了160多種操作碼。

 

      2.2 JVM寄存器

      所有的CPU均包含用於保存系統狀態和處理器所需信息的寄存器組。如果虛擬機定義較多的寄存器,便可以從中得到更多的信息而不必對棧或內存進行訪問,這有利於提高運行速度。然而,如果虛擬機中的寄存器比實際CPU的寄存器多,在實現虛擬機時就會佔用處理器大量的時間來用常規存儲器模擬寄存器,這反而會降低虛擬機的效率。針對這種情況,JVM只設置了4個最爲常用的寄存器。它們是:

(1)       pc程序計數器

(2)       optop操作數棧頂指針

(3)       frame當前執行環境指針

(4)       vars指向當前執行環境中第一個局部變量的指針

所有寄存器均爲32位。pc用於記錄程序的執行。optop, framevars用於記錄指向Java棧區的指針。

 

      2.3 JVM棧結構

      作爲基於棧結構的計算機,Java棧是JVM存儲信息的主要方法。當JVM得到一個Java字節碼應用程序後,便爲該代碼中一個類的每一個方法創建一個棧框架,以保存該方法的狀態信息。每個棧框架包括以下三類信息:

(1)       局部變量

(2)       執行環境

(3)       操作數棧

      局部變量用於存儲一個類的方法中所用到的局部變量。vars寄存器指向該變量表中的第一個局部變量。

      執行環境用於保存解釋器對Java字節碼進行解釋過程中所需的信息。它們是:(上次)調用的方法、局部變量指針和操作數棧的棧頂和棧底指針。執行環境是一個執行一個方法的控制中心。例如:如果解釋器要執行iadd(整數加法),首先要從frame寄存器中找到當前執行環境,而後便從執行環境中找到操作數棧,從棧頂彈出兩個整數進行加法運算,最後將結果壓入棧頂。

      操作數棧用於存儲運算所需操作數及運算的結果。

 

      2.4 JVM垃圾回收堆

      Java類的實例所需的存儲空間是在堆上分配的。解釋器具體承擔爲類實例分配空間的工作。解釋器在爲一個實例分配完存儲空間後,便開始記錄對該實例所佔用的內存區域的使用。一旦對象使用完畢,便將其回收到堆中。

      Java語言中,除了new語句外沒有其他方法爲一對象申請和釋放內存。對內存進行釋放和回收的工作是由Java運行系統承擔的。這允許Java運行系統的設計者自己決定垃圾回收的方法。在SUN公司開發的Java解釋器和Hot Java環境中,垃圾回收用後臺線程的方式來執行。這不但爲運行系統提供了良好的性能,而且使程序設計人員擺脫了自己控制內存使用的風險。

 

      2.5 JVM存儲區

      JVM有兩類存儲區:常量緩衝池和方法區。常量緩衝池用於存儲類名稱、方法和字段名稱以及串常量。方法區則用於存儲Java方法的字節碼。對於這兩種存儲區域具體實現方式在JVM規格中沒有明確規定。這使得Java應用程序的存儲佈局必須在運行過程中確定,依賴於具體平臺的實現方式。

       JVM是爲Java字節碼定義的一種獨立於具體平臺的規格描述,是Java平臺獨立性的基礎。目前的JVM還存在一些限制和不足,有待於進一步的完善,但無論如何,JVM的思想是成功的。

 

      對比分析:如果把Java原程序想象成我們的C++原程序,Java原程序編譯後生成的字節碼就相當於C++原程序編譯後的80x86的機器碼(二進制程序文件),JVM虛擬機相當於80x86計算機系統,Java解釋器相當於80x86CPU。在80x86CPU上運行的是機器碼,在Java解釋器上運行的是Java字節碼。

      Java解釋器相當於運行Java字節碼的“CPU”,但該“CPU”不是通過硬件實現的,而是用軟件實現的。Java解釋器實際上就是特定的平臺下的一個應用程序。只要實現了特定平臺下的解釋器程序,Java字節碼就能通過解釋器程序在該平臺下運行,這是Java跨平臺的根本。當前,並不是在所有的平臺下都有相應Java解釋器程序,這也是Java並不能在所有的平臺下都能運行的原因,它只能在已實現了Java解釋器程序的平臺下運行。

 

三、Java虛擬機JVM的體系結構

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

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

 

3  JVM的體系結構

      JVM的每個實例都有一個它自己的方法域和一個堆,運行於JVM內的所有的線程都共享這些區域;當虛擬機裝載類文件的時候,它解析其中的二進制數據所包含的類信息,並把它們放到方法域中;當程序運行的時候,JVM把程序初始化的所有對象置於堆上;而每個線程創建的時候,都會擁有自己的程序計數器和Java 棧,其中程序計數器中的值指向下一條即將被執行的指令,線程的Java棧則存儲爲該線程調用Java方法的狀態;本地方法調用的狀態被存儲在本地方法棧,該方法棧依賴於具體的實現。

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

1.執行引擎

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

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

      虛擬機的內層循環的執行過程如下:

 

do{

取一個操作符字節;

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

}while(程序未結束)

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

      第一個字節*256+第二個字節字節碼。

      指令流一般只是字節對齊的。指令tableswitchlookup是例外,在這兩條指令內部要求強制的4字節邊界對齊。

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

2Java

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

3Java方法區

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

4Java程序計數器

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

      pc: Java程序計數器;

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

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

      vars: 指向當前執行方法的局部變量區第一個變量的指針。

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

5Java

      Java虛擬機的棧有三個區域:局部變量區、運行環境區、操作數區。

      1)局部變量區

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

      2)運行環境區

      在運行環境中包含的信息用於動態鏈接、正常的方法返回以及異常捕捉。

      動態鏈接

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

      正常的方法返回

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

      異常捕捉

      異常情況在Java中被稱作Error(錯誤)Exception(異常),是Throwable類的子類,在程序中的原因是:(1)動態鏈接錯,如無法找到所需的class文件。(2)運行時錯,如對一個空指針的引用。程序使用了throw語句。

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

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

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

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

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

      3)操作數棧區

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

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

6.本地方法棧

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

 

四、Java虛擬機JVM的運行過程

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

      虛擬機通過調用某個指定類的方法main啓動,傳遞給main一個字符串數組參數,使指定的類被裝載,同時鏈接該類所使用的其它的類型,並且初始化它們。例如對於程序:

class HelloApp

{

public static void main(String[] args)

{

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

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

{

System.out.println(args);

}

}

}

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

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

      開始試圖執行類HelloAppmain方法,發現該類並沒有被裝載,也就是說虛擬機當前不包含該類的二進制代碼,於是虛擬機使用 ClassLoader試圖尋找這樣的二進制代碼。如果這個進程失敗,則拋出一個異常。類被裝載後同時在main方法被調用之前,必須對類 HelloApp與其它類進行鏈接然後初始化。鏈接包含三個階段:檢驗,準備和解析。檢驗檢查被裝載的主類的符號和語義,準備則創建類或接口的靜態域以及把這些域初始化爲標準的默認值,解析負責檢查主類對其它類或接口的符號引用,在這一步它是可選的。類的初始化是對類中聲明的靜態初始化函數和靜態域的初始化構造方法的執行。一個類在初始化之前它的父類必須被初始化。整個過程如下:

 

4JAVA虛擬機的運行過程

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章