虛擬機規範規定有且只有5種情況必須立即對類進行初始化:
- 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啓動時候,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這六種情形外,所有其他使用Java類型的方式都是被動使用,它們都不會導致Java類型的初始化被動引用:
- 通過子類引用父類的靜態字段,不會導致子類初始化。
- 通過數組定義來引用類,不會觸發此類的初始化。
- 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化接口的初始化:接口在初始化時,並不要求其父接口全部完成類初始化,只有在正整使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
在類加載階段,需要完成3件事:
1) 通過一個類的全限定名類獲取定義此類的二進制字節流。
2) 將這字節流所代表的靜態存儲結構轉化爲方法區運行時數據結構。
3) 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。
獲取二進制字節流的方式:
1) 從ZIP包中讀取,這很常見,最終成爲日後JAR、EAR、WAR格式的基礎。
2) 從網絡中獲取,這種場景最典型的應用就是Applet。
3) 運行時計算生成,這種常見使用得最多的就是動態代理技術。
4) 由其他文件生成,典型場景就是JSP應用。
5) 從數據庫中讀取,這種場景相對少一些(中間件服務器)。
數組類本身不通過類加載器創建,它是由Java虛擬機直接創建的。
數組類的創建過程遵循以下規則:
1) 如果數組的組件類型(指的是數組去掉一個維度的類型)是引用類型,那就遞歸採用上面的加載過程去加載這個組件類型,數組將在加載該組件類型的類加載器的類名稱空間上被標識。
2) 如果數組的組件類型不是引用類型(列如int[]組數),Java虛擬機將會把數組標識爲與引導類加載器關聯。
3) 數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認爲public。
(2)、驗證
驗證是連接的的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身。
驗證階段會完成下面4個階段的檢驗動作:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
1)文件格式驗證
第一階段要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。這一階段可能包括:
1. 是否以魔數oxCAFEBABE開頭。
2. 主、次版本號是否在當前虛擬機處理範圍之內。
3. 常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)。
4. 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
5. CONSTANT_Itf8_info 型的常量中是否有不符合UTF8編碼的數據。
6. Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息。
這個階段的驗證時基於二進制字節流進行的,只有通過類這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,所以後面的3個驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流。
2)元數據驗證
1. 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)。
2. 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
3. 如果這個類不是抽象類,是否實現類其父類或接口之中要求實現的所有方法。
4. 類中的字段、方法是否與父類產生矛盾(列如覆蓋類父類的final字段,或者出現不符合規則的方法重載,列如方法參數都一致,但返回值類型卻不同等)。
第二階段的主要目的是對類元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息。
3)字節碼驗證
第三階段是整個驗證過程中最複雜的一個階段,主要目的似乎通過數據流和控制流分析,確定程序語言是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。
1. 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,列如,列如在操作數棧放置類一個int類型的數據,使用時卻按long類型來加載入本地變量表中。
2. 保證跳轉指令不會跳轉到方法體以外的字節碼指令上。
3. 保證方法體中的類型轉換時有效的,列如可以把一個子類對象賦值給父類數據類型,這個是安全的,但是吧父類對象賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關係、完全不相干的一個數據類型,則是危險和不合法的。
4)符號引用驗證
發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。
1. 符號引用中通過字符串描述的全限定名是否能找到相對應的類。
2. 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
3. 符號引用中的類、字段、方法的訪問性是否可被當前類訪問。
(3)、準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量都在方法區中進行分配。這個時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。其次,這裏說的初始值通常下是數據類型的零值。
類 型 |
默認初始值 |
int |
0 |
long |
0L |
short |
(short)0 |
char |
'\u0000' |
byte |
(byte)0 |
boolean |
false |
reference |
null |
float |
0.0f |
double |
0.0d |
假設public static int value = 123,那變量value在準備階段過後的初始值爲0而不是123,因爲這時候尚未開始執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方法之中,所以把value賦值爲123的動作將在初始化階段纔會執行,但是如果使用final修飾,則在這個階段其初始值設置爲123。
(4)解析
解析階段是虛擬機將常量池內符號引用替換爲直接引用的過程
(5)初始化
類的初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才正真開始執行類中定義的Java程序代碼(或者說是字節碼)。
所有的類變量初始化語句和類型的靜態初始化語句都被Java編譯器收集在一起,放到一個特殊的方法中。對於類來說,這個方法被稱作類初始化方法;對於接口來說,它被稱爲接口初始化方法。在類和接口的Java class文件中,這個方法被稱爲“<clinit>”。這種方法只能被Java虛擬機調用。
參考資料:《深入理解Java虛擬機》