1. 程式人生 > >深入理解JVM(1)—Java虛擬機器基本結構

深入理解JVM(1)—Java虛擬機器基本結構

最近開始看周志明著的《深入理解Java虛擬機器》一書,此書作為Java虛擬機器的經典暢銷書,果然是非常優秀的,在學習它的過程中逐漸理解了Java執行機理、記憶體分配與回收等知識,收穫頗多。
要學習Java虛擬機器,首先要了解其歷史與基本構造。Java虛擬機器的發展歷史不做詳述,大家只要知道SunJDK和OpenJDK中所帶的是HotSpot虛擬機器,我們之後的學習也是基於HotSpot虛擬機器就可以了。其他還有一些其他的虛擬機器,如BEA JRockit、IBM J9VM、Microsoft JVM等,大家有興趣的也可以瞭解一下。
本節筆記基於keycoding寫的Java虛擬機器基本結構:

http://blog.csdn.net/yfqnihao,個人認為寫的非常形象易懂,有助於理解虛擬機器的內部結構。

1. 什麼是Java虛擬機器?

 百度百科給出的定義是:虛擬機器是一種抽象化的計算機,通過在實際的計算機上模擬模擬各種計算機功能來實現的。Java虛擬機器有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。JVM遮蔽了與具體作業系統平臺相關的資訊,使得Java程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。通俗地說Java虛擬機器就是處理Java程式(確切地說是Java位元組碼)的虛擬機器。

下面給出Java虛擬機器形象的說明,證明其並不是“虛擬”的,也是可以看得見的。
第一步:先來寫一個類:

    package test;  

    public class JVMTestForJava {  
        public static void main(String[] args) throws InterruptedException {  
            Thread.sleep(10000000);  
    }  
    }  

第二步:cmd視窗輸入:java test.JVMTestForJava

第三步:開啟工作管理員-程序
這裡寫圖片描述

你看到一個叫java.exe的程式沒有,是滴這個就是java的虛擬機器,java xxx這個命令就是用來啟動一個java虛擬機器,而main函式就是一個java應用的入口,main函式被執行時,java虛擬機器就啟動了。
為什麼main函式被執行的時候,Java虛擬機器就啟動了,大家可以看這篇文章

http://www.2cto.com/kf/201408/328035.html,具體介紹了Java虛擬機器的啟動,通俗地講就是通過main函式,最終啟動了JavaMain函式,該函式定義了Java虛擬機器初始化的一些引數,從而完成虛擬機器的初始化。有的人會說,我做的Java web專案也沒見到main函式啊。對於Web應用,也是有main方法的,不過不是在你的程式中,而是在應用伺服器中,如tomcat、jboss等。比如tomcat,main函式存在於tomcat的主類Bootstrap類中。

第四步:開啟你的ecplise,右鍵run application,再run application一次

第五步:開啟工作管理員-程序
這裡寫圖片描述

好了,我已經圈出來了,有兩個javaw.exe,為什麼會有兩個?因為我們剛才運行了兩次run application。這裡我是要告訴你,一個java的application對應了一個java.exe/javaw.exe(java.exe和javaw.exe你可以把它看成java的虛擬機器,一個有視窗介面一個沒有)。你執行幾個application就有幾個java.exe/javaw.exe。或者更加具體的說,你運行了幾個main函式就啟動了幾個java應用,同時也啟動了幾個java的虛擬機器。

2. Java虛擬機器的體系結構

大家可以看到Java虛擬機器內部的一個處理流程,類載入子系統與執行引擎大家可能不太熟悉,在後面會詳細講到,在這裡大家可以簡單認為類載入子系統就是載入class位元組碼檔案到虛擬機器的過程,包括載入、驗證、準備、解析、初始化等。執行引擎是Java虛擬機器最核心的組成部分之一,對載入的位元組碼檔案進行處理和解析。
對該圖進行解釋之前,首先要了解Java虛擬機器的結構佈局,而要了解Java虛擬機器的結構,我們需要知道作業系統的記憶體結構佈局。
作業系統記憶體佈局

那麼,JVM在作業系統中是如何表示的呢?
這裡寫圖片描述

那麼JVM內部又是什麼樣的佈局呢?
這裡寫圖片描述

從上圖中,你有沒有發現什麼規律,JVM的記憶體結構居然和作業系統的結構驚人的一致。 從這個圖,你應該不難發現,原來JVM的設計的模型其實就是作業系統的模型,基於作業系統的角度,JVM就是個該死的java.exe/javaw.exe,也就是一個應用,而基於class檔案來說,jvm就是個作業系統,而jvm的方法區,也就相當於作業系統的硬碟區。而java棧和作業系統棧是一致的,無論是生長方向還是管理的方式,至於堆嘛,雖然概念上一致目標也一致,分配記憶體的方式也一致(new,或者malloc等等),但是由於他們的管理方式不同,jvm是gc回收,而作業系統是程式設計師手動釋放,所以在演算法上有很多的差異,後面會講到jvm的記憶體分配與回收。

再看下圖,
這裡寫圖片描述

將這個圖和上面的圖對比多了什麼?沒錯,多了一個pc暫存器,我為什麼要畫出來,主要是要告訴你,所謂pc暫存器,無論是在虛擬機器中還是在我們虛擬機器所寄宿的作業系統中功能目的是一致的,計算機上的pc暫存器是計算機上的硬體,本來就是屬於計算機,(這一點對於學過彙編的同學應該很容易理解,有很多的暫存器eax,esp之類的32位暫存器,jvm裡的暫存器就相當於彙編裡的esp暫存器),計算機用pc暫存器來存放“偽指令”或地址,而相對於虛擬機器,虛擬機器的pc暫存器的功能也是存放偽指令,更確切的說存放的是將要執行指令的地址,它甚至可以是作業系統指令的本地地址,當虛擬機器正在執行的方法是一個本地方法的時候,jvm的pc暫存器儲存的值是undefined,所以你現在應該很明確的知道,虛擬機器的pc暫存器是用於存放下一條將要執行的指令的地址(位元組碼流)。

再對上面的圖擴充套件,這一次,我們會稍微的深入一點,看下面的圖,
這裡寫圖片描述

多了什麼?沒錯多了一個classLoader,其實這個圖是要告訴你,當一個classLoder啟動的時候,classLoader的生存地點在jvm中的堆,然後它會去主機硬碟上將A.class裝載到jvm的方法區,方法區中的這個位元組檔案會被虛擬機器拿來new A位元組碼(),然後在堆記憶體生成了一個A位元組碼的物件,然後A位元組碼這個記憶體檔案有兩個引用一個指向A的class物件,一個指向載入自己的classLoader,如下圖,
這裡寫圖片描述

那麼方法區中的位元組碼記憶體塊,除了記錄一個class自己的class物件引用和一個載入自己的ClassLoader引用之外,還記錄了什麼資訊呢??我們還是看圖,然後我會講給你聽,聽過一遍之後一輩子都不會忘記。
這裡寫圖片描述

你仔細將這個位元組碼和我們的類對應,是不是和一個基本的java類驚人的一致?下面你看我貼出的一個類的基本結構。

    package test;import java.io.Serializable;public final class ClassStruct extends Object implements Serializable {//1.類資訊  
     //2.物件欄位資訊  
     private String name;  
     private int id;  

     //4.常量池  
     public final int CONST_INT=0;  
        public final String CONST_STR="CONST_STR";  

        //5.類變數區  
        public static String static_str="static_str";  


     //3.方法資訊  
     public static final String getStatic_str ()throws Exception{  
      return ClassStruct.static_str;  
     }}  

你將上面的程式碼註解和上面的那個位元組碼碼記憶體塊按標號對應一下,有沒有發現,其實記憶體的位元組碼塊就是完整的把你整個類裝到了記憶體而已。

所以各個資訊段記錄的資訊可以從我們的類結構中得到,不需要你硬背,你認真的看過我下面的描述一遍估計就不可能會忘記了:

1.類資訊:

 修飾符(public final) 
 是類還是介面(class,interface)
 類的全限定名(Test/ClassStruct.class)
 直接父類的全限定名(java/lang/Object.class)
 直接父介面的許可權定名陣列(java/io/Serializable)

也就是 public final class ClassStruct extends Object implements Serializable這段描述的資訊提取

2.欄位資訊:

 修飾符(pirvate)
 欄位型別(java/lang/String.class)
 欄位名(name)

也就是類似private String name;這段描述資訊的提取

3.方法資訊:

 修飾符(public static final)
 方法返回值(java/lang/String.class)
 方法名(getStatic_str)
 引數需要用到的區域性變數的大小還有運算元棧大小(運算元棧我們後面會講)
 方法體的位元組碼(就是花括號裡的內容)
 異常表(throws Exception)

也就是對方法public static final String getStatic_str ()throws Exception的位元組碼的提取
4.常量池:

  4.1.直接常量:

    1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;

    1.2CONSTANT_String_info字串直接常量池   public final String CONST_STR="CONST_STR";

    1.3CONSTANT_DOUBLE_INFO浮點型直接常量池

    等等各種基本資料型別基礎常量池(待會我們會反編譯一個類,來檢視它的常量池等。)

 4.2.方法名、方法描述符、類名、欄位名,欄位描述符的符號引用

   也就是所以編譯器能夠被確定,能夠被快速查詢的內容都存放在這裡,它像陣列一樣通過索引訪問,就是專門用來做查詢的。

   編譯時就能確定數值的常量型別都會複製它的所有常量到自己的常量池中,或者嵌入到它的位元組碼流中。作為常量池或者位元組碼流的一部分,編譯時常量儲存在方法區中,就和一般的類變數一樣。但是當一般的類變數作為他們的型別的一部分資料而儲存的時候,編譯時常量作為使用它們的型別的一部分而儲存

5.類變數:

 就是靜態欄位( public static String static_str="static_str";)

 虛擬機器在使用某個類之前,必須在方法區為這些類變數分配空間。

6.一個到classLoader的引用:

通過this.getClass().getClassLoader()來取得為什麼要先經過class呢?思考一下,然後看第七點的解釋,再回來思考

7.一個到class物件的引用:

這個物件儲存了所有這個位元組碼記憶體塊的相關資訊。所以你能夠看到的區域,比如:類資訊,你可以通過this.getClass().getName()取得
所有的方法資訊,可以通過this.getClass().getDeclaredMethods(),欄位資訊可以通過this.getClass().getDeclaredFields(),等等,所以在位元組碼中你想得到的,呼叫的,通過class這個引用基本都能夠幫你完成。因為他就是位元組碼在記憶體塊在堆中的一個物件

8.方法表:

如果學習c++的人應該都知道c++的物件記憶體模型有一個叫虛表的東西,java本來的名字就叫c++- -,它的方法表其實說白了就是c++的虛表,它的內容就是這個類的所有例項可能被呼叫的所有例項方法的直接引用。也是為了動態繫結的快速定位而做的一個類似快取的查詢表,它以陣列的形式存在於記憶體中。不過這個表不是必須存在的,取決於虛擬機器的設計者,以及執行虛擬機器的機器是否有足夠的記憶體。

3.總結

Java虛擬機器在記憶體中的結構有點類似於作業系統的硬體佈局,有著自己的堆、棧、方法區、PC計數器和指令系統。說白了,就是依託現有計算機系統資源,虛擬化一個計算機用來處理Java位元組碼檔案。