深刻理解Java類的初始化順序

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類中程序執行的順序瞭如指掌。

相關文章
相關標籤/搜索