深刻理解JVM

1   Java技術與Java虛擬機java

提及Java,人們首先想到的是Java編程語言,然而事實上,Java是一種技術,它由四方面組成: Java編程語言、Java類文件格式、Java虛擬機和Java應用程序接口(Java API)。它們的關係以下圖所示:程序員

 

1   Java四個方面的關係算法

運行期環境表明着Java平臺,開發人員編寫Java代碼(.java文件),而後將之編譯成字節碼(.class文件)。最後字節碼被裝入內存,一旦字節碼進入虛擬機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。從上圖也能夠看出Java平臺由Java虛擬機和 Java應用程序接口搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程序能夠運行在這個平臺上。這個平臺的結構以下圖所示:編程

 

Java平臺的結構中, 能夠看出,Java虛擬機(JVM) 處在覈心的位置,是程序與底層操做系統和硬件無關的關鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操做系統, 其中依賴於平臺的部分稱爲適配器;JVM 經過移植接口在具體的平臺和操做系統上實現;在JVM 的上方是Java的基本類庫和擴展類庫以及它們的API, 利用Java API編寫的應用程序(application) 和小程序(Java applet) 能夠在任何Java平臺上運行而無需考慮底層平臺, 就是由於有Java虛擬機(JVM)實現了程序與操做系統的分離,從而實現了Java 的平臺無關性。小程序

那麼到底什麼是Java虛擬機(JVM)呢?一般咱們談論JVM時,咱們的意思多是:數組

  1. JVM規範的的比較抽象的說明;
  2. JVM的具體實現;
  3. 在程序運行期間所生成的一個JVM實例。

JVM規範的的抽象說明是一些概念的集合,它們已經在書《The Java Virtual Machine Specification》(《Java虛擬機規範》)中被詳細地描述了;對JVM的具體實現要麼是軟件,要麼是軟件和硬件的組合,它已經被許多生產廠商所實現,並存在於多種平臺之上;運行Java程序的任務由JVM的運行期實例單個承擔。在本文中咱們所討論的Java虛擬機(JVM)主要針對第三種狀況而言。它能夠被當作一個想象中的機器,在實際的計算機上經過軟件模擬來實現,有本身想象中的硬件,如處理器、堆棧、寄存器等,還有本身相應的指令系統。安全

JVM在它的生存週期中有一個明確的任務,那就是運行Java程序,所以當Java程序啓動的時候,就產生JVM的一個實例;當程序運行結束的時候,該實例也跟着消失了。下面咱們從JVM的體系結構和它的運行過程這兩個方面來對它進行比較深刻的研究。app

2   Java虛擬機的體系結構編程語言

剛纔已經提到,JVM能夠由不一樣的廠商來實現。因爲廠商的不一樣必然致使JVM在實現上的一些不一樣,然而JVM仍是能夠實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。函數

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

 

3   JVM的體系結構

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

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

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

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

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

do{

取一個操做符字節;

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

}while(程序未結束)

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

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

指令流通常只是字節對齊的。指令tableswitchlookup是例外,在這兩條指令內部要求強制的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的局部變量,若是是一個雙精度浮點數,那麼它實際佔據了索引nn+1所表明的存儲空間)虛擬機規範並不要求在局部變量中的64位的值是64位對齊的。虛擬機提供了把局部變量中的值裝載到操做數棧的指令,也提供了把操做數棧中的值寫入局部變量的指令。

運行環境區

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

動態連接

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

正常的方法返回

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

異常捕捉

異常狀況在Java中被稱做Error(錯誤)Exception(異常),Throwable類的子類,在程序中的緣由是:動態連接錯,如沒法找到所需的class文件。運行時錯,如對一個空指針的引用。程序使用了throw語句。

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

  • 檢查與當前方法相聯繫的catch子句表。每一個catch子句包含其有效指令範圍,可以處理的異常類型,以及處理異常的代碼塊地址。
  • 與異常相匹配的catch子句應該符合下面的條件:形成異常的指令在其指令範圍以內,發生的異常類型是其能處理的異常類型的子類型。若是找到了匹配的catch子句,那麼系統轉移到指定的異常處理塊處執行;若是沒有找到異常處理塊,重複尋找匹配的catch子句的過程,直到當前方法的全部嵌套的 catch子句都被檢查過。
  • 因爲虛擬機從第一個匹配的catch子句處繼續執行,因此catch子句表中的順序是很重要的。由於Java代碼是結構化的,所以總能夠把某個方法的全部的異常處理器都按序排列到一個表中,對任意可能的程序計數器的值,均可以用線性的順序找到合適的異常處理塊,以處理在該程序計數器值下發生的異常狀況。
  • 若是找不到匹配的catch子句,那麼當前方法獲得一個"未截獲異常"的結果並返回到當前方法的調用者,好像異常剛剛在其調用者中發生同樣。若是在調用者中仍然沒有找到相應的異常處理塊,那麼這種錯誤將被傳播下去。若是錯誤被傳播到最頂層,那麼系統將調用一個缺省的異常處理塊。

操做數棧區

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

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

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

3   Java虛擬機的運行過程

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

虛擬機經過調用某個指定類的方法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[i]);

  }

 }

}

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

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

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

 

4:虛擬機的運行過程

4   結束語

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

相關文章
相關標籤/搜索