1. 程式人生 > 實用技巧 >java虛擬機器的工作原理

java虛擬機器的工作原理

Java虛擬機器工作原理

  首先我想從巨集觀上介紹一下Java虛擬機器的工作原理。從最初的我們編寫的Java原始檔(.java檔案)是如何一步步執行的,如下圖所示,首先Java原始檔經過前端編譯器(javac或ECJ)將.java檔案編譯為Java位元組碼檔案,然後JRE載入Java位元組碼檔案,載入系統分配給JVM的記憶體區,然後執行引擎解釋或編譯類檔案,再由即時編譯器將位元組碼轉化為機器碼。主要介紹下圖中的類載入器和執行時資料區兩個部分。

  • 類載入

  類載入指將類的位元組碼檔案(.class)中的二進位制資料讀入記憶體,將其放在執行時資料區的方法區內,然後在堆上建立java.lang.Class物件,封裝類在方法區內的資料結構。類載入的最終產品是位於堆中的類物件,類物件封裝了類在方法區內的資料結構,並且向JAVA程式提供了訪問方法區內資料結構的介面。如下是類載入器的層次關係圖。

    • 啟動類載入器(BootstrapClassLoader):在JVM執行時被建立,負責載入存放在JDK安裝目錄下的jre\lib的類檔案,或者被-Xbootclasspath引數指定的路徑中,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入)。啟動類無法被JAVA程式直接引用。
    • 擴充套件類載入器(Extension ClassLoader):該類載入器負責載入JDK安裝目錄下的\jre\lib\ext的類,或者由java.ext.dirs系統變數指定路徑中的所有類庫,開發者也可以直接使用擴充套件類載入器。
    • 應用程式類載入器(AppClassLoader):負責載入使用者類路徑(Classpath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有定義過自己的類載入器,該類載入器為預設的類載入器。
    • 使用者自定義類載入器(User ClassLoader):JVM自帶的類載入器是從本地檔案系統載入標準的java class檔案,而自定義的類載入器可以做到在執行非置信程式碼之前,自動驗證數字簽名,動態地建立符合使用者特定需要的定製化構建類,從特定的場所(資料庫、網路中)取得java class。

注意如上的類載入器並不是通過繼承的方式實現的,而是通過組合的方式實現的。而JAVA虛擬機器的載入模式是一種委派模式,如上圖中的1-7步所示。下層的載入器能夠看到上層載入器中的類,反之則不行。類載入器可以載入類但是不能解除安裝類。說了一大堆,還是感覺需要拿點程式碼說事。

首先我們先定義自己的類載入器MyClassLoader,繼承自ClassLoader,並覆蓋了父類的findClass(String name)方法,如下:

View Code

  我們如何利用我們定義的類載入器載入指定的位元組碼檔案(.class)呢?如通過MyClassLoader載入C:\\Users\\Administrator\\下的Test.class位元組碼檔案,程式碼如下所示:

public class Client {
    public static void main(String[] args) {
        // TODO Auto-generated method stub        
        //MyClassLoader的父類載入器為系統預設的載入器AppClassLoader
        MyClassLoader myCLoader = new MyClassLoader("MyClassLoader");
        //指定MyClassLoader的父類載入器為ExtClassLoader
        //MyClassLoader myCLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent(),"MyClassLoader");
        myCLoader.setPath("C:\\Users\\Administrator\\");
        Class<?> clazz;
        try {
            clazz = myCLoader.loadClass("Test");
            Field[] filed = clazz.getFields();   //獲取載入類的屬性欄位
            Method[] methods = clazz.getMethods();   //獲取載入類的方法欄位
            System.out.println("該類的類載入器為:" + clazz.getClassLoader());
            System.out.println("該類的類載入器的父類為:" + clazz.getClassLoader().getParent());
            System.out.println("該類的名稱為:" + clazz.getName());
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
  • 執行時資料區

  位元組碼的載入第一步,其後分別是認證、準備、解析、初始化,那麼這些步驟又具體做了哪些工作,以及他們會對執行時資料區纏身什麼影響呢?如下圖所示:

  如下我們將介紹執行時資料區,主要分為方法區、Java堆、虛擬機器棧、本地方法棧、程式計數器。其中方法區和Java堆一樣,是各個執行緒共享的記憶體區域,而虛擬機器棧、本地方法棧、程式計數器是執行緒私有的記憶體區。

    1. Java堆:Java堆是Java虛擬機器所管理的記憶體中最大的一塊,被程序的所有執行緒共享,在虛擬機器啟動時被建立。該區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體,隨著JIT編譯器的發展與逃逸分支技術逐漸成熟,棧上分配、標量替換等優化技術使得物件在堆上的分配記憶體變得不是那麼“絕對”。Java堆是垃圾收集器管理的主要區域。由於現在的收集器基本都採用分代收集演算法,所以Java堆中還可以分為老年代和新生代(Eden、From Survivor、To Survivor)。根據Java虛擬機器規範,Java堆可以處於物理上不連續的記憶體空間,只要邏輯上連續即可。該區域的大小可以通過-Xmx和-Xms引數來擴充套件,如果堆中沒有記憶體完成例項分配,並且堆也無法擴充套件,將會丟擲OutOfMemoryError異常。
    2. 方法區:用於儲存被Java虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。不同於Java堆的是,Java虛擬機器規範對方法區的限制非常寬鬆,可以選擇不實現垃圾收集。但並非資料進入了方法區就“永久”存在了,這區域記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。如果該區域記憶體不足也會丟擲OutOfMemoryError異常。
      • 常量池:這個名詞可能大家也經常見,它是方法區的一部分。Class檔案除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池,用於存放編譯期生成的各種字面量和符號引用。Java虛擬機器執行期間,也可能將新的常量放入常量池(如String類的intern()方法)。
    3. 虛擬機器棧:執行緒私有,生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。如果請求的站深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常,虛擬機器棧在動態擴充套件時如果無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。
    4. 本地方法棧:與虛擬機器棧類似,不過虛擬機器棧是為虛擬機器執行Java方法(位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。該區域同樣會報StackOverflowError和OutOfMemoryError異常。
    5. 程式計數器:一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器完成。如果執行緒正在執行一個Java方法,計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,如果正在執行的是Native方法,這個計數器值為空。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

  寫了這麼多,感覺還是少一個例子。通過最簡單的一段程式碼解釋一下,程式在執行時資料區個部分的變化情況。

public class Test{
      public static void main(String[] args){
           String name = "best.lei";
           sayHello(name);
       }
       public static void sayHello(String name){
           System.out.println("Hello " + name);
       }
}

  通過編譯器將Test.java檔案編譯為Test.class,利用javap -verbose Test.class對編譯後的位元組碼進行分析,如下圖所示:

  我們在看看執行時資料區的變化: