由於道行不夠深,所以此篇類加載機制的講解主要來自於《深入理解Java虛擬機——JVM高級特性與最佳實踐》的第7章 虛擬機類加載機制。
在前面《初識Java反射》中我們在開頭提到要了解Java反射,就得要了解虛擬機的類加載機制。在這里,我們來試着窺探一下何為類加載。
“虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,類型的加載、連接和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。”這句話確實讀着好懂,但到底類加載做了什么呢?我們都知道Java編譯后形成.class字節碼文件,虛擬機是不認識.java文件的,所以虛擬機要加載Class文件將它做一些處理才能到“還原”成我們所寫的java程序,按照我們的邏輯步驟來執行。Java之所以稱為動態語言正是因為類型的加載、連接和初始化都是在程序運行期間完成的。這雖然會帶來一些開銷,但同時它也為Java語言帶來了很大的靈活性。
那么在此期間虛擬機做了什么呢?我們可以通過下面的圖來了解類的整個生命周期:加載、驗證、准備、解析、初始化、使用、卸載。
這7個階段中的:加載、驗證、准備、初始化、卸載的順序是固定的。但它們並不一定是嚴格同步串行執行,它們之間可能會有交叉,但總是以“開始”的順序總是按部就班的。至於解析則有可能在初始化之后才開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。
對於加載階段(注意加載和類加載概念,加載是類加載過程的第一個階段)JVM並沒有對此約束,而是交由虛擬機的具體實現。但對於初始化,虛擬機規范則做了嚴格的規定,初始化可能也是對我們實際編程運用當中非常值得注意的問題。
虛擬機對於類的初始化階段嚴格規定了有且僅有只有5種情況如果對類沒有進行過初始化,則必須對類進行“初始化”!
- 遇到new、讀取一個類的靜態字段(getstatic)、設置一個類的靜態字段(putstatic)、調用一個類的靜態方法(invokestatic)。
- 使用java.lang.reflect包的方法對類進行反射調用時。
- 當類初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。(如果是接口,則不必觸發其父類初始化)
- 當虛擬機執行一個main方法時,會首先初始化main所在的這個主類。
- 當只用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。(暫未研究此種場景)
上面5種場景是有且僅有,稱之為“主動引用”,只有滿足上述5種場景之一,就會觸發對類進行初始化。其余都不會觸發類初始化,稱之為“被動引用”。
下面列舉3個例子來說明何為“被動引用”:
被動引用——例1
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class SuperClass { 9 static{ 10 System.out.println("SuperClass init!"); 11 } 12 public static int value = 123; 13 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class SubClass extends SuperClass { 9 static{ 10 System.out.println("SubClass init!"); 11 } 12 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 System.out.println(SubClass.value); 15 } 16 17 }
輸出結果:
我們看到雖然我們是通過子類來調用的父類靜態字段,但從結果可以看到並沒有初始化子類,而是初始化了父類,這即是“被動引用”。對於靜態字段,只有直接定義這個字段的類才會被初始化,通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
被動引用——例2
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 SuperClass[] sca = new SuperClass[10]; 15 } 16 17 }
我們還是利用例1的SuperClass來創建一個數組,這個應該都知道,在創建數組時並不會初始化,在編程不注意的時候可能常常因為沒有初始化數組導致空指針的情況。它僅僅做了創建一個大小為10的數組。
被動引用——例3
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class ConstClass { 9 static { 10 System.out.println("ConstClass init!"); 11 } 12 13 public static final String HELLO = "hello"; 14 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 System.out.println(ConstClass.HELLO); 15 } 16 17 }
這個例子的輸出會初始化ConstClass類嗎?
答案是並不會。這是因為常量在編譯階段會存入調用類的常量池中,本質上並沒有直接飲用到定義常量的類。進一步解釋,雖然在main方法中引用了ConstClass類中的常量HELLO,但其實在編譯階段通過常量傳播優化,已經將此常量的值“hello”存儲到了Main類的常量池中,之后對ConstClass.HELLO的引用實際上都被轉化為Main類對自身常量池的引用。也就是說,兩個類在編譯過后實際上不存在任何聯系了。
類加載時機就講到這里了,類加載的初始化步驟非常重要的一個步驟,理解清楚“初始化”對我們寫出高質量不易出錯的代碼非常重要。