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:父類的靜態代碼塊->子類的靜態代碼塊->初始化父類的屬性值/父類的普通代碼塊(自上而下的順序排列)->父類的構造方法->初始化子類的屬性值/子類的普通代碼塊(自上而下的順序排列)->子類的構造方法。
注:構造函數最後執行。數據結構
上面的例子只是小試牛刀,接下來再看一個比較複雜的例子:ide
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類加載中具體作了些什麼。spa
JAVA類的加載機制
Java類加載分爲5個過程,分別爲:加載,鏈接(驗證,準備,解析),初始化,使用,卸載。設計
1. 加載
加載主要是將.class文件(也能夠是zip包)經過二進制字節流讀入到JVM中。 在加載階段,JVM須要完成3件事:
1)經過classloader在classpath中獲取XXX.class文件,將其以二進制流的形式讀入內存。
2)將字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
3)在內存中生成一個該類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。code
2.鏈接對象
2.1. 驗證
主要確保加載進來的字節流符合JVM規範。驗證階段會完成如下4個階段的檢驗動做:
1)文件格式驗證
2)元數據驗證(是否符合Java語言規範)
3)字節碼驗證(肯定程序語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行)
2.2. 準備
準備是鏈接階段的第二步,主要爲靜態變量在方法區分配內存,並設置默認初始值。
2.3. 解析
解析是鏈接階段的第三步,是虛擬機將常量池內的符號引用替換爲直接引用的過程。
3. 初始化
初始化階段是類加載過程的最後一步,主要是根據程序中的賦值語句主動爲類變量賦值。
當有繼承關係時,先初始化父類再初始化子類,因此建立一個子類時其實內存中存在兩個對象實例。
注:若是類的繼承關係過長,單從類初始化角度考慮,這種設計不太可取。緣由我想你已經猜到了。
一般建議的類繼承關係最多不超過三層,即父-子-孫。某些特殊的應用場景中可能會加到4層,但就此打住,第4層已經有代碼設計上的弊端了。
4. 使用
程序之間的相互調用。
5. 卸載
即銷燬一個對象,通常狀況下中有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類中程序執行的順序瞭如指掌。