虛擬機是如何加載類的

科技優家 2017-06-23

一、概述

首先先來看幾個問題

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

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

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

與那些在編譯是需要進行連接工作的語言不同,在Java語言裡面,類型的加載和連接過程都是在程序運行期間完成的,這樣會在類加載是稍微增加一些性能開銷,但是卻能為Java應用程序提供高度的靈活性,Java中可以動態的擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實現的。比如編寫一個使用接口的應用程序,可以等到運行時在指定其實際的實現。這種組裝應用程序的方式廣泛應用於Java程序之中。

二、要點

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

虛擬機是如何加載類的

1、加載

加載階段是“類加載”過程的一個階段,虛擬機需要做以下三件事:

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

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

2、驗證

驗證階段虛擬機做了下面這些事情

1、文件格式驗證

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

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

2、元數據的驗證

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

3、字節碼的驗證

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

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

4、符號引用驗證

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

3、準備

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

虛擬機是如何加載類的

4、解析

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

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

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

5、初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全有虛擬機主導和空值。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

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

三、初始化階段補充

一下四中情況會必須立即對類進行“初始化”。

  • 遇到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

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

四、總結

本篇文章依據以下兩點

  • 在實際情況中,每個class文件都有可能代表著Java語言中的一個類或者接口,而對於類和接口需要分開描述
  • 筆者所講的“Class文件”並非指class必須是存在於具體磁盤中的某個文件,這裡說的class文件指的是遺傳二進制字節流,無論以何種形式存在都可以。

相關推薦

推薦中...