Java類的載入機制(類載入和初始化順序)
Java類載入機制中最重要的就是程式初始化過程,其中包含了靜態資源,非靜態資源,父類子類,構造方法之間的執行順序。這類知識經常會出現在面試題中,如果沒有搞清楚其原理,在複雜的開源設計中可能無法梳理其業務流程,是java程式設計師進階的阻礙。
首先通過一個例子來分析java程式碼的執行順序:
public class CodeBlockForJava extends BaseCodeBlock { { System.out.println("這裡是子類的普通程式碼塊"); } public CodeBlockForJava() { System.out.println("這裡是子類的構造方法"); } @Override public void msg() { System.out.println("這裡是子類的普通方法"); } public static void msg2() { System.out.println("這裡是子類的靜態方法"); } static { System.out.println("這裡是子類的靜態程式碼塊"); } public static void main(String[] args) { BaseCodeBlock bcb = new CodeBlockForJava(); bcb.msg(); } Other o = new Other(); } class BaseCodeBlock { public BaseCodeBlock() { System.out.println("這裡是父類的構造方法"); } public void msg() { System.out.println("這裡是父類的普通方法"); } public static void msg2() { System.out.println("這裡是父類的靜態方法"); } static { System.out.println("這裡是父類的靜態程式碼塊"); } Other2 o2 = new Other2(); { System.out.println("這裡是父類的普通程式碼塊"); } } class Other { Other() { System.out.println("初始化子類的屬性值"); } } class Other2 { Other2() { System.out.println("初始化父類的屬性值"); } }
這個例子比較簡單,在執行程式碼之前分析一下:帶有static關鍵字的程式碼塊應該是最先執行,其次是非static關鍵字的程式碼塊以及類的屬性(Fields),最後是構造方法。帶上父子類的關係後,上面的執行結果為:
這裡是父類的靜態程式碼塊
這裡是子類的靜態程式碼塊
初始化父類的屬性值
這裡是父類的普通程式碼塊
這裡是父類的構造方法
這裡是子類的普通程式碼塊
初始化子類的屬性值
這裡是子類的構造方法
這裡是子類的普通方法
注意的是類的屬性與非靜態程式碼塊的執行級別是一樣的,誰先執行取決於書寫的先後順序。
結論1:父類的靜態程式碼塊->子類的靜態程式碼塊->初始化父類的屬性值/父類的普通程式碼塊(自上而下的順序排列)->父類的構造方法->初始化子類的屬性值/子類的普通程式碼塊(自上而下的順序排列)->子類的構造方法。
注:建構函式最後執行。
上面的例子只是小試牛刀,接下來再看一個比較複雜的例子:
public class ClassloadSort1 { public static void main(String[] args) { Singleton.getInstance(); System.out.println("Singleton value1:" + Singleton.value1); System.out.println("Singleton value2:" + Singleton.value2); Singleton2.getInstance2(); System.out.println("Singleton2 value1:" + Singleton2.value1); System.out.println("Singleton2 value2:" + Singleton2.value2); } } class Singleton { static { System.out.println(Singleton.value1 + "\t" + Singleton.value2 + "\t" + Singleton.singleton); //System.out.println(Singleton.value1 + "\t" + Singleton.value2); } private static Singleton singleton = new Singleton(); public static int value1 = 5; public static int value2 = 3; private Singleton() { value1++; value2++; } public static Singleton getInstance() { return singleton; } int count = 10; { System.out.println("count = " + count); } } class Singleton2 { static { System.out.println(Singleton2.value1 + "\t" + Singleton2.value2 + "\t" + Singleton2.singleton2); } public static int value1 = 5; public static int value2 = 3; private static Singleton2 singleton2 = new Singleton2(); private String sign; int count = 20; { System.out.println("count = " + count); } private Singleton2() { value1++; value2++; } public static Singleton2 getInstance2() { return singleton2; } }
這個用例相比第一個,知識點更深了一層。如果你用結論1是沒法分析出正確答案的,但這並不代表結論1就是錯誤的。
執行結果:
Singleton value1:5
Singleton value2:3
Singleton2 value1:6
Singleton2 value2:4
Singleton中的value1,value2並沒有受到構造方法中自加操作的影響。然而Singleton2中的程式碼也相同,為什麼執行出來的效果就不一樣呢?
要想知道原因,必須先搞清楚Java類載入中具體做了些什麼。
JAVA類的載入機制
Java類載入分為5個過程,分別為:載入,連線(驗證,準備,解析),初始化,使用,解除安裝。
- 載入
載入主要是將.class檔案(也可以是zip包)通過二進位制位元組流讀入到JVM中。 在載入階段,JVM需要完成3件事:
1)通過classloader在classpath中獲取XXX.class檔案,將其以二進位制流的形式讀入記憶體。
2)將位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
3)在記憶體中生成一個該類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
2.1. 驗證
主要確保載入進來的位元組流符合JVM規範。驗證階段會完成以下4個階段的檢驗動作:
1)檔案格式驗證
2)元資料驗證(是否符合Java語言規範)
3)位元組碼驗證(確定程式語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行)
2.2. 準備
準備是連線階段的第二步,主要為靜態變數在方法區分配記憶體,並設定預設初始值。
2.3. 解析
解析是連線階段的第三步,是虛擬機器將常量池內的符號引用替換為直接引用的過程。
-
初始化
初始化階段是類載入過程的最後一步,主要是根據程式中的賦值語句主動為類變數賦值。
當有繼承關係時,先初始化父類再初始化子類,所以建立一個子類時其實記憶體中存在兩個物件例項。
注:如果類的繼承關係過長,單從類初始化角度考慮,這種設計不太可取。原因我想你已經猜到了。
通常建議的類繼承關係最多不超過三層,即父-子-孫。某些特殊的應用場景中可能會加到4層,但就此打住,第4層已經有程式碼設計上的弊端了。 -
使用
程式之間的相互呼叫。 -
解除安裝
即銷燬一個物件,一般情況下中有JVM垃圾回收器完成。程式碼層面的銷燬只是將引用置為null。
通過上面的整體介紹後,再來看Singleton2.getInstance()的執行分析:
1)類的載入。執行Singleton2.getInstance(),JVM在首次並沒有發現Singleton類的相關資訊。所以通過classloader將Singleton.class檔案載入到記憶體中。
2)類的驗證。略
3)類的準備。將Singleton2中的靜態資源轉化到方法區。value1,value2,singleton在方法區被宣告分別初始為0,0,null。
4)類的解析。略(將常量池內的符號引用替換為直接引用的過程)
5)類的初始化。執行靜態屬性的賦值操作。按照順序先是value1 = 5,value2 = 3,接下來是private static Singleton2 singleton2 = new Singleton2();
這是個建立物件操作,根據 結論1 在執行Singleton2的構造方法之前,先去執行static資源和非static資源。但由於value1,value2已經被初始化過,所以接下來執行的是非static的資源,最後是Singleton2的構造方法:value1++;value2++。
所以Singleton2結果是6和4。
以上除了搞清楚執行順序外,還有一個重點->結論2:靜態資源在類的初始化中只會執行一次。不要與第3個步驟混淆。
有了以上的這個結論,再來看Singleton.getInstance()的執行分析:
1)類的載入。將Singleton類載入到記憶體中。
2)類的驗證。略
3)類的準備。將Singleton2的靜態資源轉化到方法區。
4)類的解析。略(將常量池內的符號引用替換為直接引用的過程)
5)類的初始化。執行靜態屬性的賦值操作。按照順序先是private static Singleton singleton = new Singleton(),根據 結論1 和結論2,value1和value2不會在此層執行賦值操作。所以singleton物件中的value1,value2只是在0的基礎上進行了++操作。此時singleton物件中的value1=1,value2=1。
然後, public static int value1 = 5; public static int value2 = 3; 這兩行程式碼才是真的執行了賦值操作。所以最後的結果:5和3。
如果執行的是public static int value1; public static int value2;結果又會是多少?結果: 1和1。
注:為什麼 Singleton singleton = new Singleton()不會對value1,value2進行賦值操作?因為static變數的賦值在類的初始化中只會做一次。
程式在執行private static Singleton singleton = new Singleton()時,已經是對Singleton類的static變數進行賦值操作了。這裡new Singleton()是一個特殊的賦值,類似於遞迴裡層,外層已經是賦值操作了,所以裡層會自動過濾static變數的賦值操作。但非static的變數依然會被賦值。
結論3:在結論2的基礎上,非靜態資源會隨物件的建立而執行初始化。每建立一個物件,執行一次初始化。
掌握結論1,2,3基本對java類中程式執行的順序瞭如指掌。這還不夠,ClassLoader還沒有介紹呢。
類載入器
JVM提供了以下3種系統的類載入器:
- 啟動類載入器(Bootstrap ClassLoader):最頂層的類載入器,負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
- 擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
- 應用程式類載入器(Application ClassLoader):也叫做系統類載入器,可以通過getSystemClassLoader()獲取,負責載入使用者路徑(classpath)上的類庫。如果沒有自定義類載入器,一般這個就是預設的類載入器。