java虛擬機器的執行原理

y_keven發表於2013-07-17

一、類載入器

首先來看一下java程式的執行過程。

                         

從這個框圖很容易大體上了解java程式工作原理。首先,你寫好java程式碼,儲存到硬碟當中。然後你在命令列中輸入

  1. javac YourClassName.java  
javac YourClassName.java

此時,你的java程式碼就被編譯成位元組碼(.class).如果你是在Eclipse IDE或者其他開發工具中,你儲存程式碼的時候,開發工具已經幫你完成了上述的編譯工作,因此你可以在對應的目錄下看到class檔案。此時的class檔案依然是儲存在硬碟中,因此,當你在命令列中執行

  1. java YourClassName  
java YourClassName

就完成了上面紅色方框中的工作。JRE的來載入器從硬碟中讀取class檔案,載入到系統分配給JVM的記憶體區域--執行資料區(Runtime Data Areas). 然後執行引擎解釋或者編譯類檔案,轉化成特定CPU的機器碼,CPU執行機器碼,至此完成整個過程。


接下來就重點研究一下類載入器究竟為何物?又是如何工作的?

首先看一下來載入器的一些特點,有點抽象,不過總有幫助的。


》》層級結構

類載入器被組織成一種層級結構關係,也就是父子關係。其中,Bootstrap是所有類載入器的父親。如下圖所示:

        

--Bootstrap class loader:

當執行java虛擬機器時,這個類載入器被建立,它載入一些基本的java API,包括Object這個類。需要注意的是,這個類載入器不是用java語言寫的,而是用C/C++寫的。

--Extension class loader:

這個載入器載入出了基本API之外的一些擴充類,包括一些與安全效能相關的類。(目前瞭解得不是很深,只能籠統說,待日後再詳細說明)

--System Class Loader:

它載入應用程式中的類,也就是在你的classpath中配置的類。

--User-Defined Class Loader:

這是開發人員通過擴充ClassLoader類定義的自定義載入器,載入程式設計師定義的一些類。


》》委派模式(Delegation Mode)

仔細看上面的層次結構,當JVM載入一個類的時候,下層的載入器會將將任務委託給上一層類載入器,上一層載入檢查它的名稱空間中是否已經載入這個類,如果已經載入,直接使用這個類。如果沒有載入,繼續往上委託直到頂部。檢查完了之後,按照相反的順序進行載入,如果Bootstrap載入器找不到這個類,則往下委託,直到找到類檔案。對於某個特定的類載入器來說,一個Java類只能被載入一次,也就是說在Java虛擬機器中,類的完整標識是(classLoader,package,className)。一個雷可以被不同的類載入器載入。


舉個具體的例子來說明,現在加入我有一個自己定義的類MyClass需要載入,如果不指定的話,一般交App(System)載入。接到任務後,System檢查自己的庫裡是否已經有這個類,發現沒有之後委託給Extension,Extension進行同樣的檢查,發現還是沒有繼續往上委託,最頂層的Boots發現自己庫裡也沒有,於是根據它的路徑(Java 核心類庫,如java.lang)嘗試去載入,沒找到這個MaClass類,於是只好(人家看好你,交給你完成,你無能為力,只好交給別人啦)往下委託給Extension,Extension到自己的路徑(JAVA_HOME/jre/lib/ext)是找,還是沒找到,繼續往下,此時System載入器到classpath路徑尋找,找到了,於是載入到Java虛擬機器。

現在假設我們將這個類放到JAVA_HOME/jre/lib/ext這個路徑中去(相當於交給Extension載入器載入),按照同樣的規則,最後由Extension載入器載入MyClass類,看到了吧,統一各類被兩次載入到JVM,但是每次都是由不同的ClassLoader完成。


》》可見性限制

下層的載入器能夠看到上層載入器中的類,反之則不行,也就是是說委託只能從下到上。


》》不允許解除安裝類

類載入器可以載入一個類,但是它不能解除安裝一個類。但是類載入器可以被刪除或者被建立。


當類載入完畢之後,JVM繼續按照下圖完成其他工作:


框圖中各個步驟簡單介紹如下:

Loading:文章前面介紹的類載入,將檔案系統中的Class檔案載入到JVM記憶體(執行資料區域)

Verifying:檢查載入的類檔案是否符合Java規範和虛擬機器規範。

Preparing:為這個類分配所需要的記憶體,確定這個類的屬性、方法等所需的資料結構。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)

Resolving:將該類常量池中的符號引用都改變為直接引用。(不是很理解)

Initialing:初始化類的區域性變數,為靜態域賦值,同時執行靜態初始化塊。


那麼,Class Loader在載入類的時候,究竟做了些什麼工作呢?

要了解這其中的細節,必須得先詳細介紹一下執行資料區域。


二、執行資料區域

Runtime Data Areas:當執行一個JVM示例時,系統將分配給它一塊記憶體區域(這塊記憶體區域的大小可以設定的),這一記憶體區域由JVM自己來管理。從這一塊記憶體中分出一塊用來儲存一些執行資料,例如建立的物件,傳遞給方法的引數,區域性變數,返回值等等。分出來的這一塊就稱為執行資料區域。執行資料區域可以劃分為6大塊:Java棧、程式計數暫存器(PC暫存器)、本地方法棧(Native Method Stack)、Java堆、方法區域、執行常量池(Runtime Constant Pool)。執行常量池本應該屬於方法區,但是由於其重要性,JVM規範將其獨立出來說明。其中,前面3各區域(PC暫存器、Java棧、本地方法棧)是每個執行緒獨自擁有的,後三者則是整個JVM例項中的所有執行緒共有的。這六大塊如下圖所示:


》PC計數器:

每一個執行緒都擁有一個PC計數器,當執行緒啟動(start)時,PC計數器被建立,這個計數器存放當前正在被執行的位元組碼指令(JVM指令)的地址。

》Java棧:

同樣的,Java棧也是每個執行緒單獨擁有,執行緒啟動時建立。這個棧中存放著一系列的棧幀(Stack Frame),JVM只能進行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當呼叫一個方法時,JVM就往棧裡壓入一個棧幀,方法結束返回時彈出棧幀。如果方法執行時出現異常,可以呼叫printStackTrace等方法來檢視棧的情況。棧的示意圖如下:


OK。現在我們再來詳細看看每一個棧幀中都放著什麼東西。從示意圖很容易看出,每個棧幀包含三個部分:本地變數陣列,運算元棧,方法所屬類的常量池引用。

》區域性(本地)變數陣列:

區域性(本地)變數陣列中,從0開始按順序存放方法所屬物件的引用、傳遞給方法的引數、區域性變數。舉個例子:

  1. public void doSomething(int a, double b, Object o) {  
  2. ...  
  3. }  
public void doSomething(int a, double b, Object o) {
...
}

這個方法的棧幀中的區域性變數儲存的內容分別是:

  1. 0this  
  2. 1: a  
  3. 2,3:b  
  4. 4:0  
0: this
1: a
2,3:b
4:0

看仔細了,其中double型別的b需要兩個連續的索引。取值的時候,取出的是2這個索引中的值。如果是靜態方法,則陣列第0個不存放this引用,而是直接儲存傳遞的引數。

》運算元棧:

運算元棧中存放方法執行時的一些中間變數,JVM在執行方法時壓入或者彈出這些變數。其實,運算元棧是方法真正工作的地方,執行方法時,區域性變數陣列與運算元棧根據方法定義進行資料交換。例如,執行以下程式碼時,運算元棧的情況如下:

  1. int a = 90;  
  2. int b = 10;  
  3. int c = a + b;  
int a = 90;
int b = 10;
int c = a + b;


注意在這個圖中,運算元棧的地步是在上邊,所以先壓入的100位於上方。可以看出,運算元棧其實是一個資料臨時儲存區,存放一些中間變數,方法結束了,運算元棧也就沒有啦。

》棧幀中資料引用:

除了區域性變數陣列和運算元棧之外,棧幀還需要一個常量池的引用。當JVM執行到需要常量池的資料時,就是通過這個引用來訪問常量池的。棧幀中的資料還要負責處理方法的返回和異常。如果通過return返回,則將該方法的棧幀從Java棧中彈出。如果方法有返回值,則將返回值壓入到呼叫該方法的方法的運算元棧中。另外,資料區中還儲存中該方法可能的異常表的引用。下面的例子用來說明:

  1. class Example3C{  
  2.     public static void addAndPrint(){  
  3.         double result = addTwoTypes(1,88.88);  
  4.         System.out.println(result);  
  5.     }  
  6.     public static double addTwoTypes(int i, double d){  
  7.     return i+d;  
  8.     }  
  9.   
  10. }  
class Example3C{
    public static void addAndPrint(){
        double result = addTwoTypes(1,88.88);
        System.out.println(result);
    }
    public static double addTwoTypes(int i, double d){
    return i+d;
    }

}

執行上述程式碼時,Java棧如下圖所示:


花些時間好好研究上圖。一樣需要注意的是,棧的底部在上方,先押人員addAndPrint方法的棧幀,再壓入addTwoTypes方法的棧幀。上圖最右邊的文字說明有錯誤,應該是addTwoTypes的執行結果存放在addAndPrint的運算元棧中。

》》本地方法棧

當程式通過JNI(Java Native Interface)呼叫本地方法(如C或者C++程式碼)時,就根據本地方法的語言型別建立相應的棧。

》》方法區域

方法區域是一個JVM例項中的所有執行緒共享的,當啟動一個JVM例項時,方法區域被建立。它用於存執行放常量池、有關域和方法的資訊、靜態變數、類和方法的位元組碼。不同的JVM實現方式在實現方法區域的時候會有所區別。Oracle的HotSpot稱之為永久區域(Permanent Area)或者永久代(Permanent Generation)。

》》執行常量池

這個區域存放類和介面的常量,除此之外,它還存放方法和域的所有引用。當一個方法或者域被引用的時候,JVM就通過執行常量池中的這些引用來查詢方法和域在記憶體中的的實際地址。

》》堆(Heap)

堆中存放的是程式建立的物件或者例項。這個區域對JVM的效能影響很大。垃圾回收機制處理的正是這一塊記憶體區域。

所以,類載入器載入其實就是根據編譯後的Class檔案,將java位元組碼載入JVM記憶體,並完成對執行資料處於的初始化工作,供執行引擎執行。


三、 執行引擎(Execution  Engine)

類載入器將位元組碼載入記憶體之後,執行引擎以Java 位元組碼指令為但願,讀取Java位元組碼。問題是,現在的java位元組碼機器是讀不懂的,因此還必須想辦法將位元組碼轉化成平臺相關的機器碼。這個過程可以由直譯器來執行,也可以有即時編譯器(JIT Compiler)來完成。


相關文章