JVM 類加載機制

前言

在java文件被編譯成class文件存儲爲二進制字節碼後,並不能直接使用,通過類加載,一個類才能夠被裝載進運行時內存並被使用。所以理解類加載機制才能讓咱們更深入地理解咱們編寫的java代碼是如何一步一步的編譯成class文件,到如何在內存中正確的使用的過程。複製代碼

類加載的時機

這裏寫圖片描述
這裏寫圖片描述

類從被加載到虛擬機內存開始,到卸載出內存爲止,它的生命週期如上圖。其中,驗證、準備和解析3個部分統稱爲鏈接。

加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,而解析則不必定,解析主要目的是將符號引用轉換爲直接引用,它某些狀況能夠在初始化之後纔開始,這是爲了支持java的運行時綁定。java

關於加載何時開始,jvm規範中並無明確約束,由不一樣虛擬機本身把握,但對於初始化階段,虛擬機規範嚴格規定有且只有5種狀況必須對類進行初始化:數組

  1. 遇到new、getstaic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則會觸發初始化。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則會先觸發其初始化。
  3. 當初始化一個類的時候,若是發現其父類還沒進行過初始化,則須要觸發其父類初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類,虛擬機會先初始化這個主類。
  5. 當使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則會先觸發其初始化。

類加載的過程

加載

加載階段,虛擬機須要完成3件事:安全

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

上面說獲取二進制字節流,而沒有明確的說明是class文件中的字節流,由於還有其它獲取字節流的方式,例如從jar包中獲取、從網絡中獲取、動態代理運行時生成等。bash

加載階段與鏈接階段的部份內容是交叉進行的,如:一部分字節碼文件格式驗證動做。加載階段還沒有完成,鏈接階段可能已經開始了。網絡

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成如下4個階段的校驗動做:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。數據結構

1.文件格式驗證

這一階段目的是驗證二進制字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,檢測內容包括如下幾點:jvm

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

這個階段是基於二進制字節流進行的,只有經過了這個階段的驗證,字節流纔會流入方法區中進行存儲,後面3個階段全是基於方法區的存儲結構進行的,不會再直接操做字節流。函數

2.元數據驗證

這一階段主要對字節碼的描述信息進行語義分析,以保證其描述信息符合java語言規範,這階段的驗證點可能包括如下幾點:ui

  • 這個類是否有父類
  • 這個類的父類是否繼承了不被容許繼承的類(被final 修飾)
  • 若是這個類不是抽象類,是否實現了父類或接口中的方法
  • 類中的字段、方法是否與父類產生矛盾(覆蓋父類的final字段值等)

3.字節碼驗證

這一階段目的主要目的是肯定程序語義是合法的、符合邏輯的。這個階段主要對類的字節碼進行校驗分析,保證該類的方法不會在運行時作出危害虛擬機安全的事:編碼

  • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出險操做數棧上 int 類型的數據使用時按long類型加載進本地變量表中。
  • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
  • 保證方法體內的類型轉化是有效的,能夠把一個子類對象賦值給父類數據結構,這是安全的,而不能把父類賦值給子類甚至與它無關係的數據類型,這是危險和不合法的。

4.符號引用驗證

這一階段用來將符號引用轉換爲直接引用的時候,這個轉化將在解析階段中發生,符號引用驗證能夠看作是類對自身之外(常量池中各類符號引用)的信息進行匹配性校驗,一般須要校驗如下內容:

  • 符號引用中可否根據字符串的權限定名找到對應的類。
  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱描述的方法和字段。
  • 符號引用中的類、字段、方法的訪問性是否能夠被當前類訪問。

準備

準備階段是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區分配。實例變量會在對象實例化的時候跟對象一塊兒在java堆中分配。這裏的初始值指的是一般狀況下的零值。假設一個類變量定義爲:

public static int a=123;

那麼變量a初始化的值是0而不是123。若是變量同時是final類型,那麼準備階段就會被賦值爲123,沒必要等到初始化階段再賦值。

解析

解析階段是將虛擬機常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行。可能你們有疑問Class文件中哪有這麼多內容,其實上面也說了,是針對常量池。不論是CLass文件中的方法表仍是字段表,不能直接表示的內容,基本都會直接或間接存在常量池中,所以解析過程就是針對常量池中的數據類型進行解析的。

1.類或接口的解析

要把一個從未解析過的符號引用N解析爲一個類或接口的直接引用,虛擬機須要完成如下3個步驟:

  1. 若是C不是一個數組類型,那麼虛擬機會把表明N的權限定名傳遞給D的類加載器去加載這個類C。在加載的過程中,因爲元數據、字節碼驗證的操做,又可能觸發其它類的加載動做,一旦出險任何異常,則解析宣告失敗。
  2. 若是C是一個數組類型,而且數組元素爲對象,描述符相似「[Ljava/lang/Integer」的形式按照第一點的規則加載數組元素類型。若是N的描述符如前面所假設的形式,須要加載的類型就是java.lang.Integer,接着由虛擬機生成一個表明次數組維度和元素的數組對象。
  3. 若是上面的步驟沒有任何異常,那麼C在虛擬機中實際上已經稱爲一個有效的類或接口了。解析以前還要進行符號驗證,確認D是否具備對C的訪問權限,若是不具有則會拋出異常。

2.字段解析

對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用,若是解析這個類或符號引用的過程當中出現任何異常,都會致使字段符號引用解析的失敗。若是解析成功,這個字段對應的類或接口用C表示,接下來沿着A和它的父類/父接口尋找是否有這個字段,若是有會進行權限驗證,若是不具有權限則拋出異常。若是這個過程不出錯,則會在找到符合字段的時候返回這個字段的直接飲用,查找結束。

3.類(靜態)方法解析

類方法解析首先也要首先解析出類方法表class_index項中索引的方法所屬的類或接口的符號引用,解析成功用C表示。

  1. 類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中索引類是個接口,直接拋出異常。
  2. 若是經過了第一步,在類C中查找是否有簡單名稱和描述符都與目標匹配的方法,有則返回這個方法的直接引用,查找結束。
  3. 不然在類的父類遞歸查找是否有這個方法,有則返回直接引用,查找結束。
  4. 不然在類的接口列表和父接口遞歸查找,若是存在匹配的方法,說明類C是一個抽象類,查找結束,拋出異常。
  5. 不然宣告查找失敗,拋出異常。

最後若是查找成功返回了直接引用,還要對這個方法進行權限驗證,若是不具有權限,則會拋出異常。

接口方法解析

接口方法須要先解析出接口方法表的class_index 項中索引的方法所屬的類或接口的符號引用。

  1. 若是發現class_index 中的索引C是個類而不是接口,直接拋出異常。
  2. 不然在接口C中查找是否有描述符和名稱都匹配的方法,有則返回方法的直接引用,查找結束。
  3. 不然在其父接口中遞歸查找,匹配就返回方法的直接引用,查找結束。
  4. 不然宣告方法查找失敗。

初始化

類初始化是類加載過程的最後一步。前面的類加載過程當中,除了加載階段能夠自定義類加載器干預以外,其他動做徹底由虛擬機主導。到了初始化階段,才真正開始執行java代碼。

咱們知道,在前面的準備階段,已經對類變量分配過內存並設置初始值。在初始化階段,則是爲類變量或其它資源設置程序中聲明的值。注意這裏仍然是類變量,不包括實例變量。或者明確的說,這一階段,是執行static關鍵字修飾的變量或代碼塊。本質上,初始化是執行類構造器
<client>方法的過程。

<client>方法是由編譯器自動收集類中全部類變量的賦值動做和靜態代碼塊中的語句合併產生的。編譯器收集的順序是有語句在資源文件中出險的順序所決定的。

所以平時可能會遇到這種問題:以下代碼

public class Client {
    private static Client client = new Client();

    public static int a;
    public static int b = 0;

    private Client() {
        a++;
        b++;
    }

    public static Client getInstance() {
        return client;
    }

    public static void main(String[] args) {
        Client instance = Client.getInstance();
        System.out.println("a= " + Client.a);
        System.out.println("b= " + Client.b);
    }

}複製代碼

輸出結果是

a= 1
b= 0複製代碼

可能有人問爲何,其實把類加載的過程邏輯理清楚,也不是問題。咱們知道在類加載的準備階段會給類變量分配內存和賦初始值。在外部調用Client.getInstance()時,由於以前類沒有被加載過,會引起類加載,到了準備階段就會給類變量賦初始值。賦值順序同一個類中是按聲明的順序,也就是

client=null;
a=0;
b=0複製代碼

而後解析完開始初始化,按程序聲明的值給類變量賦值。首先執行clinet=new Client(),其實關鍵就是這裏new的過程會調用構造函數,調用完後

a=1;
b=1;複製代碼

接着繼續初始化,a只是聲明沒有賦值,因此沒有任何操做,b聲明且賦值爲0,因此初始化完成後

a=1;
b=0;複製代碼
相關文章
相關標籤/搜索