一文理解java物件初始化順序
例子
Talk is cheap, Show you the code!
public class ParentClass { static int parentStaticField = 1; final static int parentFinalStaticField = 2; int parentSubField = 3; static { System.out.println("parent static!"); System.out.println("SubStatic field:" + SubClass.subStaticField); System.out.println("SubFinalStatic field:" + SubClass.subFinalStaticField); System.out.println("ParentFinalStatic field:" + parentStaticField); System.out.println("ParentFinalStatic field:" + parentFinalStaticField); } { System.out.println("parent not static!"); System.out.println("SubStatic field:" + SubClass.subStaticField); System.out.println("SubFinalStatic field:" + SubClass.subFinalStaticField); System.out.println("ParentFinalStatic field:" + parentStaticField); System.out.println("ParentFinalStatic field:" + parentFinalStaticField); System.out.println("Parent field:" + parentSubField); } public ParentClass() { System.out.println("parent construct!"); } } public class SubClass extends ParentClass { static int subStaticField = 1; final static int subFinalStaticField = 2; int subField = 3; static { System.out.println("Sub static!"); System.out.println("SubStatic field:" + SubClass.subStaticField); System.out.println("SubFinalStatic field:" + SubClass.subFinalStaticField); } { System.out.println("Sub no static!"); System.out.println("SubStatic field:" + SubClass.subStaticField); System.out.println("SubFinalStatic field:" + SubClass.subFinalStaticField); } public SubClass() { System.out.println("Sub Construct!"); } public static void main(String[] args) { new SubClass(); } } 輸出: parent static! --- 父類靜態程式碼塊 SubStatic field:0 SubFinalStatic field:2 ParentFinalStatic field:1 --- 父類靜態程式碼塊呼叫之前靜態屬性已經初始化 ParentFinalStatic field:2 Sub static! --- 子類靜態程式碼塊 SubStatic field:1 SubFinalStatic field:2 parent not static! --- 父類非靜態程式碼塊 SubStatic field:1 SubFinalStatic field:2 ParentFinalStatic field:1 ParentFinalStatic field:2 Parent field:3 parent construct! --- 父類建構函式 Sub no static! --- 子類非靜態程式碼塊 SubStatic field:1 SubFinalStatic field:2 Sub Construct! --- 子類建構函式
從上面的例子總結類初始化的時候程式碼的執行順序如下:
父類和子類的
final static
屬性初始化 ---》 父類的static
屬性初始化---》父類的static
程式碼塊---》子類的
static
屬性初始化---》子類的static
程式碼塊 ---》父類的非靜態屬性 ---》父類的非靜態程式碼塊---》父類的建構函式 ---》 子類的非靜態屬性---》 子類的非靜態程式碼塊---》子類的構造含不管父類還是子類,靜態還是非靜態,屬性的初始化時間在程式碼塊執行之前
static final
修飾的屬性,不管是父類還是子類,在類載入的準備階段就已經初始化了,他要優先於任何程式碼塊和屬性的初始化
細說
類的載入階段
1、載入
由類載入器負責根據一個類的全限定名來讀取此類的二進位制位元組流到JVM內部,並存儲在執行時記憶體區的方法區,然後將其轉換為一個與目標型別對應的java.lang.Class物件例項
2、驗證
格式驗證:驗證是否符合class檔案規範
語義驗證:檢查一個被標記為final的型別是否包含子類;檢查一個類中的final方法是否被子類進行重寫;
確保父類和子類之間沒有不相容的一些方法宣告(比如方法簽名相同,但方法的返回值不同)
操作驗證:在運算元棧中的資料必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否可以通過符號引用中描述的全限定名定位到指定型別上,以及類成員資訊的訪問修飾符是否允許訪問等)
3、準備
為類中的所有靜態變數分配記憶體空間,併為其設定一個初始值(由於還沒有產生物件,例項變數不在此操作範圍內)
被final修飾的static變數(常量),會直接賦值;
4、解析
將常量池中的符號引用轉為直接引用(得到類或者欄位、方法在記憶體中的指標或者偏移量,以便直接呼叫該方法),這個可以在初始化之後再執行。
解析需要靜態繫結的內容。 // 所有不會被重寫的方法和域都會被靜態繫結
以上2、3、4三個階段又合稱為連結階段,連結階段要做的是將載入到JVM中的二進位制位元組流的類資料資訊合併到JVM的執行時狀態中。
5、初始化(先父後子)
為靜態變數賦值
執行static程式碼塊
注意:static程式碼塊只有jvm能夠呼叫
如果是多執行緒需要同時初始化一個類,僅僅只能允許其中一個執行緒對其執行初始化操作,其餘執行緒必須等待,只有在活動執行緒執行完對類的初始化操作之後,才會通知正在等待的其他執行緒。
因為子類存在對父類的依賴,所以類的載入順序是先載入父類後加載子類,初始化也一樣。不過,父類初始化時,子類靜態變數的值也有有的,是預設值。
最終,方法區會儲存當前類類資訊,包括類的靜態變數、類初始化程式碼(定義靜態變數時的賦值語句 和 靜態初始化程式碼塊)、例項變數定義、例項初始化程式碼(定義例項變數時的賦值語句例項程式碼塊和構造方法)和例項方法,還有父類的類資訊引用。
物件建立階段
1、在堆區分配物件需要的記憶體
分配的記憶體包括本類和父類的所有例項變數,但不包括任何靜態變數
2、對所有例項變數賦預設值
將方法區內對例項變數的定義拷貝一份到堆區,然後賦預設值
3、執行例項初始化程式碼
初始化順序是先初始化父類再初始化子類,初始化時先執行例項程式碼塊然後是構造方法
4、如果有類似於Child c = new Child()形式的c引用的話,在棧區定義Child型別引用變數c,然後將堆區物件的地址賦值給它
需要注意的是,每個子類物件持有父類物件的引用,可在內部通過super關鍵字來呼叫父類物件,但在外部不可訪問
補充:
通過例項引用呼叫例項方法的時候,先從方法區中物件的實際型別資訊找,找不到的話再去父類型別資訊中找。
如果繼承的層次比較深,要呼叫的方法位於比較上層的父類,則呼叫的效率是比較低的,因為每次呼叫都要經過很多次查詢。這時候大多系統會採用一種稱為虛方法表的方法來優化呼叫的效率。
所謂虛方法表,就是在類載入的時候,為每個類建立一個表,這個表包括該類的物件所有動態繫結的方法及其地址,包括父類的方法,但一個方法只有一條記錄,子類重寫了父類方法後只會保留子類的。當通過物件動態繫結方法的時候,只需要查詢這個表就可以了,而不需要挨個查詢每個父類。
參考連結
- https://mp.weixin.qq.com/s/gLTgqWRdb6Mi6P1Tq9h1Rg