虛擬機是如何加載類的

1、概述

  首先先來看幾個問題java

  • jvm是如何加載這些Class文件的?
  • jvm加載一個Class文件須要哪些步驟?
  • Class文件中的信息進入到虛擬機後會發生什麼變化?

  接下來看看jvm加載class文件的概述:程序員

  jvm把描述類的數據從class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。這句話差很少已經回答上面三個問題的大部分了。數組

  與那些在編譯是須要進行鏈接工做的語言不一樣,在Java語言裏面,類型的加載和鏈接過程都是在程序運行期間完成的,這樣會在類加載是稍微增長一些性能開銷,可是卻能爲Java應用程序提供高度的靈活性,Java中能夠動態的擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特色實現的。好比編寫一個使用接口的應用程序,能夠等到運行時在指定其實際的實現。這種組裝應用程序的方式普遍應用於Java程序之中。數據結構

2、要點

  類從被加載到jvm內存中開始,到卸載出內存爲止,它的生命週期包括了一下步驟:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Useing)和卸載(Unloading)七個階段。其中的驗證、準備和解析三個部分統稱爲連接(Linking),這七個階段的發生順序以下圖,注意是發生的順序,不是執行完成的前後順序。jvm

一、加載

  加載階段是「類加載」過程的一個階段,虛擬機須要作如下三件事:佈局

  • 經過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲方法區這些數據的入口。

  加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式有虛擬機實現自定義,虛擬機規範未規定此區域的具體數據結構。而後再Java堆中實例化一個java.lang.Class類的對象,這個對象做爲程序訪問方法區中的這些類型數據的外部接口。加載階段與鏈接階段的部份內容是交替進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。性能

二、驗證

  驗證階段虛擬機作了下面這些事情編碼

  一、文件格式驗證spa

  第一階段是要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。會驗證一下這些內容。指針

  • 主、次版本號是否在當前虛擬機處理範圍以內。
  • 常量池的常量中是否有不被支持的常量類型。
  • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據。
  • Class文件中各部分及文件自己是否有被刪除的或附加的其餘信息

  二、元數據的驗證

  • 這個類是否有父類。
  • 這個類的父類是否繼承了不容許被繼承的類(即被final修飾的類)。
  • 若這個類不是抽象類,是否實現了其父類或接口之中要求的全部方法。

  三、字節碼的驗證

  這個階段是驗證最爲複雜的一個階段,主要工做是進行數據流和控制流分析,緊接第二階段。

  • 保證任意時刻操做數棧的數據類型與指令代碼序列能配合工做。不會是在操做棧中放置了一個int類型的數據,使用時卻按照long類型來加載。
  • 保證跳轉指令不會跳轉到方法體以外的字節碼上。
  • 保證 方法體中的類型轉換是有效。

  四、符號引用驗證

  • 符號引用中經過字符串描述的全限定名是否能找到對應的類。
  • 在知道類中是否存在符合方法的字段描述符即簡單名稱所描述的方法和字段。
  • 符合引用中的類、字段和方法的訪問級別是否能夠被當前的類訪問。

三、準備

  準備階段是正式爲類變量分配內存並設置類變量初始值的階段,注意是初始值不是最終變量的值,都將在方法區中進行分配。若是該變量不是靜態變量,將不會進行內存分配,而是會在類出乎實話的時候隨着對象一塊兒分配到Java堆中。另外這裏的初始值一般狀況下是零值。具體的初始化的值見下圖,圖片來源於《深刻理解Jvm虛擬機》。

 四、解析

  解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

  •  符號引用:符號引用以一組符號來描述所引用的目標,符號可使任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用於虛擬機實現內存佈局無關,引用的目標不必定已經加載到內存中。

  •  直接引用:直接引用可使直接指向目標的指針、相對偏移量是一個能間接定位到目標的句柄。

 五、初始化

   類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底有虛擬機主導和空值。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

  前面講到在準備階段變量已經富餘過一次初始值,而在初始化階段,則是根據程序員經過程序制定主觀計劃去初始化變量和其餘資源。

3、初始化階段補充

  一下四中狀況會必須當即對類進行「初始化」。

  • 遇到new、getstatic、putstatic或invokestatic者4調字節碼指令時,若是類每一進行過初始化,則須要先觸發器初始化。
  • 使用反射調用一個對象的時候,該對象必須初始化
  • 當初始化一個類的時候發現其父類沒有初始化,則對其父類先初始化
  • 當虛擬機啓動的時候,用戶須要知道一個要執行的主類(即包含main()方法的那個類),虛擬機會先初始化這個主類。

  除了上面4中場景,都不會觸發初始化,稱爲被動引用。

場景一

public class SupClass {

    public static int value = 100;
    
    static{
        System.out.println("SupClass init...");
    }
    
}


public class SubClass extends SupClass {
    
    static{
        System.out.println("SubClass init...");
        
    }
    
}

 客戶端代碼

public class InitTest {
    
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
    
}

 輸出以下

SupClass init...
100

 能夠看到經過子類引用父類的靜態字段,不會致使子類初始化。

 場景二

 其餘代碼同場景一,客戶端代碼變成以下

public class InitTest {
    
    public static void main(String[] args) {
        SupClass[] sca = new SupClass[10];
    }
    
}

 這段代碼不會輸出任何結果。由於經過數組定義來引用類,不會觸發此類的初始化。

 場景三

public class ConstClass {

    public static final String HELLO = "hello";
    
    static{
        System.out.println("ConstClass init...");
    }
    
}

 客戶端代碼

public class InitTest {
    
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);
    }
    
}

 輸出以下

hello

 能夠看到常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。

4、總結

本篇文章依據如下兩點

  • 在實際狀況中,每一個class文件都有可能表明着Java語言中的一個類或者接口,而對於類和接口須要分開描述
  • 筆者所講的「Class文件」並不是指class必須是存在於具體磁盤中的某個文件,這裏說的class文件指的是一種二進制字節流,不管以何種形式存在均可以。
相關文章
相關標籤/搜索