關於類初始化過程網上有不少相關的文章,其實也算是學習語言時一個基礎知識,但今天我想從字節碼錶現上更深刻的來理解各類場景下的實例初始化過程是怎麼樣的,從簡單到複雜大致分爲下面幾個場景
。
一、成員+構造函數
二、成員+代碼塊+構造函數
三、
靜態變量+靜態代碼塊
四、
繼承和多態
首先明確下運行環境:
咱們先來看一下第一個場景:
成員+構造函數,也是咱們最常用到的場景。在這個場景中想經過字節碼瞭解下成員屬性的初始狀況,下圖左邊代碼
聲明瞭四個類成員屬性和一個無入參構造函數
,右邊是對應的執行字節碼棧幀執行順序。
一、在字節碼(1)處隱示的調用了類的父默認構造函數,這個很重要,決定了類的多重初始化過程,詳細在最後一個場景展開;
二、代碼中myId1和myId2屬性聲明方式不一樣,初始化過程也是不同的,如圖中所示,myId1在聲明屬性時未進行任何的指令操做,而是等到構造函數中的myId1=100時纔有執行指令,而像myId2在聲明就進行賦值指令,因此myId2會被myId2優先初始化。
三、myText2在聲明時賦於null,因此咱們能夠看到指令也會進行aconst_null的操做,可是在(6)時再次對myText2進行了賦值並再次產生了指令操做。注:null自己不是一種對象,在JVM中沒有明確的指明採用什麼類型,不一樣的JVM實現可能不同,咱們能夠簡單理解爲null是一個標誌,告訴虛擬機對應的類型還不明確,並還未爲其分配空間。
四、從這個場景圖的右邊字節碼指令執行過程咱們能夠總結出:
》
有賦值的類
成員屬性是按聲明的位置前後進行初始化(與訪問標誌符無關),如圖(2)(3);
》成員屬性的初始化會優先於構造函數的初始化,如圖(3)(6);
》初始化動做都是在構造函數中完成的,
若是沒有顯示構造函數,那麼編譯器會產生一個無入參構造函數來完成初始工做;
》建議聲明成員屬性時沒有必要賦於null,等到真實須要使用成成員時再初始化或傳遞值;
再來看第二個場景,若是咱們的類中有非靜態的代碼塊時,總體初始化是怎麼樣的,先來看下面的代碼示例兩個print方法最終會輸出什麼內容。
最終會輸出:
text:null
text:text1-1
咱們從字節碼上分析一下爲何會是這個輸出結果。
一、從圖的字節碼上能夠看出,非靜態代碼塊的執行最終也是被放進構造函數中完;
二、代碼塊與成員屬性的初始化順序也是按其在代碼中出現的前後順序,如(2)(3)所示;
三、因此在執行(1)print時,實際上myText1尚未被任何的初始化,包括成員屬性的賦值,因此這時輸出text:null;
四、實際上
myText1是在(2)纔有值,但緊接着還會被(3)給替換,所
在執行(4)時輸出text:text1-1;
五、該場景總結:
》非靜態代碼塊的執行也是被放到構造函數中。
》非靜態代碼塊並不影響代碼順序的初始化工做
》儘可能不要有非靜態的代碼塊,可讀性很差,須要在非靜態代碼塊解決的問題徹底能夠移到構造函數中。
接下來咱們來看第三個場景,一樣咱們先來看一個代碼輸出結果 ,下面的代碼最終輸出的什麼?
咱們再來一下靜態變量與靜態代碼段的字節碼是什麼樣的:
一、從圖上咱們發現靜態量和靜態代碼塊編譯後都整合到一個static段中,如(1)(2)(3),這個段中的字節碼執行順序就是靜態變量和靜態代碼塊在源代碼中的出現順序,並且該static{}最終在虛擬機加載類時調用一次;
二、而(4)(5)雖然實例化兩個對象,但與static{}裏的執行碼沒有任何的關係;
三、靜態變量和靜態代碼塊的聲明順序決定了引用順序,好比圖上代碼中的(6)是一個不合法的引用,由於myStaticId2在其後面聲明;
四、總結
:上面執行結果是一個200,是的就一個,由於靜態變量和靜態代碼塊與所在的類被實例化個數無關,而是所在類被虛擬機加載時會執行對應的靜態代碼塊的字節碼,這是類被加載事件觸發的,並會由於類實例纔會有,好比第一次執行下面的非實例化的代碼一樣會觸發該類的靜態代碼塊執行。 注:並不是代碼中類一出現就會進行加載靜態初始化,有些被編譯本地化的代碼就會不執行,好比使用某個類聲明時已經賦值的static+final的屬性時。
System.out.println(StaticFieldInitialize.class);
//或
Object obj = new Object();
if(obj instanceof StaticFieldInitialize){
}
最後一個場景繼承的初始化過程,若是理解上面三個場景後,繼承能夠拆成下面兩個步驟來看初始化,第一步執行父類的初始化過程,第二步執行自己類初始化過程,若是父類還有父類重複這兩個步驟,而每個步驟都遵循下面過程:
一、一性次:優先加載類時會初始化靜態成員/靜態代碼塊 ,順序爲父類 -》子類,每一個類在JVM只會被初始化一次,除非類被卸載再加載;
二、接着會按繼承關係執行:父類成員屬性/非靜態代碼塊 -》父類構造函數 -》成員屬性/非靜態代碼 -》 構造函數;
可是說到繼承咱們更多的時候會考慮到多態的過程,下面一塊兒來分析理解動態的執行場景,一樣先來看兩段代碼,以下圖:
這是一個很是簡單繼承關係,Chlid繼承了Parent類並重寫了printName方法,是個典型的多態特性,那麼在執行Child類的mian方法後會輸出什麼呢?答案是輸出child = null,
若是你已經充分理解了多態性那麼你可能很短的時間也得出這個答案,可是這個過程是怎麼樣的呢,咱們仍是一步一步分析下:
一、在第一個場景中咱們知道在Child類初始化本身構造函數時,第一步會優先調用它的父類構造函數,而這時Child類的name屬性還未被賦值,即它仍是null;
二、在執行父類構造函數時,調用Object構造函數後,先執行Parent類的name成員賦值的字節碼,緊張着會調用printName方法,但從源代碼層面看這彷佛就是調用Parent的printName方法,可是事實非如此,咱們看一下Parent類的字節碼。
從字節碼上咱們能夠看到,在調用printName的前一個棧幀爲 aload_0指令,這個指令是指將
當前的局部變量數組中下標爲0
的引用壓進棧,而全部的實例化的對象局部變量數組下標爲0的引用都是this,而this這個關鍵字與運行時多態緊密相關,也就是說你在代碼中看到的this在運行時有可能不代碼當前表實例自己,有多是子類傳遞的引用,咱們能夠經過debug方式進一步驗證,我在Parent類構造函數調用printName處下個斷點進行debug。
從上面運行過程的圖中能夠發現,此時在Parent類中的this並非代碼Parent實例自己,而是表明Child類的實例引用,因此此時運行調用時會優先到Child實例上去尋找printName方法,若是有該方法就執行,沒有則執行父類的。
三、當經過運行時動態調用Child實例printName方法時,Child類的name屬性還未初始化,因此看到輸出
child = null的結果。
最後,須要再提的一個是若是一個類有多個構造函數,那麼這個類的成員變量實例化和非靜態代碼塊的字節碼指令會在全部的構造數中都生成。
系列:
參考資料: