1. 程式人生 > >類加載的時機和過程

類加載的時機和過程

java虛擬機 ons 可見 nsa 綁定 順序 屬性 main 發現

概述

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類加載的時機

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為鏈接(Linking),這7個階段的發生順序如下圖所示:

技術分享圖片

上圖中,加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這個順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。註意,這裏筆者寫的是按部就班地“開始”,而不是按部就班地“進行”或“完成”,強調這點是因為這些階段通常都是互相交叉地混合式進行地,通常會在一個階段執行的過程中調用、激活另外一個階段。

對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。

2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

4)當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

5)當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

類加載的過程

加載

在加載階段,虛擬機需要完成以下3件事情:

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

對於數組而言,情況就有所不同,數組本身不通過類加載器創建,它是由Java虛擬機直接創建的。一個數據類(下面簡稱為C)創建過程就遵循以下規則:

  • 如果數組的組建類型(Component Type,指的是數組去掉一個維度的類型)是引用類型,那就遞歸采用本節中定義的加載過程去加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間上被標識。
  • 如果數組的組建類型不是引用類型(例如int[]數組),Java虛擬機將會把數組C標記為與引導類加載器關聯。
  • 數組類的可見性與它的組建類型的可見性一致,如果組建類型不是引用類型,那數組類的可見性將默認為public。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲結構由虛擬機實現自行定義,虛擬機規範未規定此區域的具體數據結構。然後在內存中實例化一個java.lang.Class類的對象,這個對象將作為程序訪問方法區中的這些類型數據的外部接口。

驗證

驗證是鏈接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

驗證階段大致上會完成下面4個階段的校驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

1)文件格式驗證

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

該階段的主要目的是確保輸入的字節流能正確地解析並存儲於方法區之內,格式上符合描述一個Java類型信息地要求。

2)元數據驗證

第二階段是對字節碼描述的信息進行語義分析,以確保其描述的信息符合Java語言規範的要求。

第二階段的主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息。

3)字節碼驗證

第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

如果一個類方法的字節碼沒有通過字節碼驗證,那肯定是有問題的;但如果一個方法通過了字節碼驗證,也不能說明其一定就是安全的。

4)符號引用驗證

最後一個階段的校驗發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。

該階段的目的是確保解析動作能正常執行,如果無法通過符號引用驗證,那麽將會拋出一個java.lang.IncompatibleClassChangeError異常的子類。

對於虛擬機對的類加載機制來說,驗證階段是一個非常重要的,但不是一定必要(因為對程序運行期沒有影響)的階段。

準備

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。這裏所說的初始值“通常情況”下是數據類型的零值,假設一個變量的定義為:

public static int value = 123;

那變量value在準備階段過後的初始值為0而不是123,因為這時候尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。下表列出了所有Java基礎類型的零值:

數據類型 零值 數據類型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char ‘\u0000‘ reference null
byte (byte)0

上面提到,在“通常情況”下初始值是零值,那相對的會有一些“特殊情況”:如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ConstantValue屬性所指定的值,假設上面類變量value的定義變為:

public static final int value =123;

編譯時javac將會為value生成ConsantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為123。

解析

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

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位目標的句柄。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

1)類或接口的解析

假設當前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機完成整個解析的過程需要以下3個步驟:

  1. 如果C不是一個數組類型,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C。
  2. 如果C是一個數組類型,並且數組的元素類型為對象,也就是N的描述符會是類似“[Ljava/lang/Integer]”的形式,那將會按照第1點的規則加載數組元素類型。
  3. 如果上面的步驟沒有出現任何異常,那麽C在虛擬機中實際上已經成為一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證,確認D是否具備對C的訪問權限。

2)字段解析

要解析一個未被解析過的字段符號引用,首先將會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用。

  1. 如果C本身就包含了簡單名稱和字段描述都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  2. 否則,如果在C中實現了接口,將會按照繼承關系從下往上遞歸搜索各個接口和它的夫接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  3. 否則,如果C不是java.lang.Object的話,將會按照繼承關系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  4. 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。

3)類方法解析

類方法解析的第一個步驟與字段解析一樣,也需要先解析出類放發表的class_index項中索引的方法所屬的類或接口的符號引用,如果成功解析成功,我們依然用C表示這個類,接下來虛擬機將會按照如下步驟進行後續的類方法搜索。

  1. 類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中的索引的C是個接口,那直接拋出java.lang.IncompatibleClassChangeError異常。
  2. 如果通過第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  3. 否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  4. 否則,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,這時查找結束,拋出java.lang.AbstractMethodError異常。
  5. 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

4)接口方法解析

接口方法也需要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功,依然用C表示這個接口,接下來虛擬機將會按照如下步驟進行進行後續的接口方法搜索。

  1. 與類方法解析不同,如果在接口方法表中發現class_index中的索引C是個類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
  2. 否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  3. 否則,在接口C的父接口中遞歸查找,知道java.lang.Object類(查找範圍會包括Object類)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  4. 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。

在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序指定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器()方法的過程。

類加載的時機和過程