JVM的類加載

JVM的類加載

##類的生命週期java

類的生命週期分爲如下幾個階段:加載驗證準備解析初始化使用卸載。其中驗證、準備、解析3個部分統稱爲鏈接(Linking)。 對於初始化階段,虛擬機嚴格規範了有且只有5種狀況必須當即對類進行「初始化」:數組

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

##加載數據結構

在加載階段,虛擬機須要完成如下3件事情:多線程

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

數組類自己不經過類加載器建立,它是由Java虛擬機直接建立的,可是數據的元素類型(數組去掉全部維度的類型)最終是要經過類加載器去建立,一個數組類C的建立過程遵循如下規則:線程

  1. 若是數組的組件類型(數組去掉一個維度的類型)是引用類型,那就遞歸採用上面的加載過程區加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間上被標識。
  2. 若是數組的組件類型不是引用類型,Java虛擬機將會把數組C標記爲與引導類加載器關聯。
  3. 數組的可見性與它的組件類型的可見性一致,若是組件類型不是引用類型,那數組的可見性將默認爲public。

##驗證code

###1. 文件格式驗證 目的:保證輸入的字節流能正確的解析並存儲於方法去中。 ###2. 元數據驗證 目的:對類的元數據信息進行校驗。 ###3. 字節碼驗證 目的:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。將對類的方法體進行校驗分析。 ###4. 符號引用驗證 目的:確保解析動做能正常執行。對象

##準備繼承

準備階段是正式爲類變量分配內存並設置類變量初始值的階段(這裏所說的初始值「一般狀況」下是數據類型的零值),這些類變量所使用的內存都將在方法區中分配。 這個階段進行內存分配的僅包含類變量,不包含實例變量,實例變量將在對象實例化時隨着對象一塊兒在java堆中分配。 假設一個類變量的定義以下:遞歸

public static int value = 123;

那變量value在準備階段事後的初始值是0而不是123,由於這個時候還沒有執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放在類構造器的<clinit>方法之中,因此把value賦值爲123的動做將在初始化階段執行。 一般狀況下初始值是零值,還有特殊狀況:若是類的字段屬性表中存在ConstantValue屬性,那在準備階段該字段就會被初始化爲ConstantValue屬性所指定的值。 假設上面類變量value的定義以下:索引

public static final int value = 123;

編譯時`Javac將會爲value生成ConstantValue屬性,在準備階段``虛擬機就會根據ConstantValue的設置將value賦值爲123;

##解析

###類或接口的解析(CONSTANT_ Class_info) 假設當前類爲D,要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,解析過程以下:

  1. 若是C不是數組類型,虛擬機將會把表明N的全限定名傳遞給D的類加載器去加載類C。在加載過程當中,因爲元數據驗證、字節碼驗證的須要,可能觸發其餘類的加載動做(例如這個類的父類或實現的接口),若加載過程當中出現了任何異常,解析過程就會失敗。
  2. 若是C是數組類型,而且數組的元素類型是對象(即N的描述符相似「[Ljava/lang/Integer」形式),就會按照第1點的規則加載數組元素類型(即須要加載的元素類型就是「java.lang.Integer」),接着由虛擬機生成一個表明此數組維度和元素的數組對象。
  3. 若是上述過程未出現任何異常,那麼類C在虛擬機中已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認類D是否具有對類C的訪問權限,若發現不具有,將拋出java.lang.IllegalAccessError異常。

###字段解析(CONSTANT_Fieldref_info)

  1. 要解析一個從未被解析過的字段的符號引用,首先會對字段表內class_index項索引的CONSTANT_Class_info符號引用進行解析,若解析成功,用C表示這個字段所屬的類或接口。
  2. 若是C自己就包含了簡單名稱和字段描述符與目標相匹配的字段,則返回這個字段的直接飲用,查找結束。
  3. 不然,若是C實現了接口,則按照繼承關係從下往上遞歸搜索各個接口和它的父接口,若接口中包含了簡單名稱和字段描述與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  4. 不然,若是C不是java.lang.Object類型,將會按照繼承關係從下往上遞歸搜索其父類,若父類中包含了簡單名稱和字段描述與目標相匹配的字段,則返回這個字段的直接飲用,查找結束。
  5. 不然,查找失敗,拋出java.lang.NoSuchFieldError異常。

若是成功查找到直接引用,會對這個字段進行權限驗證,若是發現不具有對字段的訪問權限,拋出java.lang.IllegalAccessError異常。 實際狀況,虛擬機的編譯器實現可能比上述過程更加嚴格:若是一個同名字段同時出如今C的父類和接口中,或同時在本身和父類的多個接口中出現,那編譯器可能會拒絕編譯。

###類方法解析(Class_Methodref_info)

  1. 首先對方法表中class_index項索引的CONSTANT_Class_info符號引用進行解析,若解析成功,用C表示這個類。
  2. 類方法和接口方法符號引用的常量類型定義是分開的,若是在發如今方法表中的class_index字段索引的C是接口,則拋出java.lang.IncompatibleClassChangeError異常。
  3. 不然,若是C類中包含了簡單名稱和描述符與目標相匹配的方法,則返回這個方法的直接引用,查找結束。
  4. 不然,在C的直接父類中遞歸查找是否包含了簡單名稱和描述符與目標相匹配的方法,若是包含,則返回這個方法的直接引用,查找結束。
  5. 不然,在C實現的接口和它們的父接口中遞歸查找是否包含簡單名稱和描述符與目標相匹配的方法,若是包含,則說明C是一個抽象類,此時查找結束,拋出一個java.lang.AbstractMethodError異常。
  6. 不然,查找失敗,拋出java.lang.NoSuchMethodError異常。

若是成功查找到方法的直接引用,將會對這個方法進行權限驗證,若是發現不具有對這個方法的訪問權限,則拋出java.lang.IllegalAccessError異常。

###接口方法解析(Class_InterfaceMethodref_info)

  1. 首先對接口方法表中class_index項索引的CONSTANT_InterfaceMethodref_info符號引用進行解析,若解析成功用C表示這個接口。
  2. 若是C是個類,則直接拋出java.lang.IncompatibleClassChangeError異常。
  3. 不然,在接口C中查找是否包含簡單名稱和描述符都與目標相匹配的方法,若是包含,則返回這個字段的直接引用,查找結束。
  4. 不然,在接口C的父接口中遞歸查找,直到java.lang.Object類(包括Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,若果有,則返回這個方法的直接引用,查找結束。
  5. 不然,查找失敗,拋出java.lang.NoSuchMethodError異常。

因爲接口中全部方法的默認訪問權限都是public的,因此不存在訪問權限的問題,也就不會拋出java.lang.IllegalAccessError異常。

##初始化

初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法可能會影響程序運行行爲的特色和細節:

  • <clinit>()方法是由編譯器自動收集類中類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是按照語句在源文件中出現的順序決定的,靜態語句塊中只能訪問定義在靜態語句塊以前的類變量。
  • <clinit>()方法不須要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object。
  • 接口中不能使用靜態語句塊,接口與類不一樣的是:執行接口的<clinit()>方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會初始化。接口的實現類在進行初始化時也不會執行接口的<clinit>()方法。
  • 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,如有多個線程同時初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動的線程執行完<clinit>()方法。

參考:

  • 《深刻理解Java虛擬機(第2版)》周志明 著
  • 《Java虛擬機規範 java se7》
相關文章
相關標籤/搜索