Java類初始化的順序經常讓人犯迷糊,現在本文嘗試着從JVM的角度,對Java非繼承和繼承關係中類的初始化順序進行試驗,嘗試給出JVM角度的解釋。
非繼承關係中的初始化順序
對於非繼承關係,主類InitialOrderWithoutExtend中包含了靜態成員變量(類變量)SampleClass 類的一個實例,普通成員變量SampleClass 類的2個實例(在程序中的順序不一樣)以及一個靜態代碼塊,其中靜態代碼塊中如果靜態成員變量sam不爲空,則改變sam的引用。main()方法中創建了2個主類對象,打印2個主類對象的靜態成員sam的屬性s。
代碼1:
package com.j2se;
public class InitialOrderWithoutExtend {
static SampleClass sam = new SampleClass("靜態成員sam初始化");
SampleClass sam1 = new SampleClass("普通成員sam1初始化");
static {
System.out.println("static塊執行");
if (sam == null)
System.out.println("sam is null");
sam = new SampleClass("靜態塊內初始化sam成員變量");
}
SampleClass sam2 = new SampleClass("普通成員sam2初始化");
InitialOrderWithoutExtend() {
System.out.println("InitialOrderWithoutExtend默認構造函數被調用");
}
public static void main(String[] args) {
// 創建第1個主類對象
System.out.println("第1個主類對象:");
InitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();
// 創建第2個主類對象
System.out.println("第2個主類對象:");
InitialOrderWithoutExtend ts2 = new InitialOrderWithoutExtend();
// 查看兩個主類對象的靜態成員:
System.out.println("2個主類對象的靜態對象:");
System.out.println("第1個主類對象, 靜態成員sam.s: " + ts.sam);
System.out.println("第2個主類對象, 靜態成員sam.s: " + ts2.sam);
}
}
class SampleClass {
// SampleClass 不能包含任何主類InitialOrderWithoutExtend的成員變量
// 否則導致循環引用,循環初始化,調用棧深度過大
// 拋出 StackOverFlow 異常
// static InitialOrderWithoutExtend iniClass1 = new InitialOrderWithoutExtend("靜態成員iniClass1初始化");
// InitialOrderWithoutExtend iniClass2 = new InitialOrderWithoutExtend("普通成員成員iniClass2初始化");
String s;
SampleClass(String s) {
this.s = s;
System.out.println(s);
}
SampleClass() {
System.out.println("SampleClass默認構造函數被調用");
}
@Override
public String toString() {
return this.s;
}
}
輸出結果:
靜態成員sam初始化
static塊執行
靜態塊內初始化sam成員變量
第1個主類對象:
普通成員sam1初始化
普通成員sam2初始化
InitialOrderWithoutExtend默認構造函數被調用
第2個主類對象:
普通成員sam1初始化
普通成員sam2初始化
InitialOrderWithoutExtend默認構造函數被調用
2個主類對象的靜態對象:
第1個主類對象, 靜態成員sam.s: 靜態塊內初始化sam成員變量
第2個主類對象, 靜態成員sam.s: 靜態塊內初始化sam成員變量
由輸出結果可知,執行順序爲:
- static靜態代碼塊和靜態成員
- 普通成員
- 構造函數執行
當具有多個靜態成員和靜態代碼塊或者多個普通成員時,初始化順序和成員在程序中申明的順序一致。
注意到在該程序的靜態代碼塊中,修改了靜態成員sam的引用。main()方法中創建了2個主類對象,但是由輸出結果可知,靜態成員和靜態代碼塊只進行了一次初始化,並且新建的2個主類對象的靜態成員sam.s是相同的。由此可知,類的靜態成員和靜態代碼塊在類加載中是最先進行初始化的,並且只進行一次。該類的多個實例共享靜態成員,靜態成員的引用指向程序最後所賦予的引用。
繼承關係中的初始化順序
此處使用了3個類來驗證繼承關係中的初始化順序:Father父類、Son子類和Sample類。父類和子類中各自包含了非靜態代碼區、靜態代碼區、靜態成員、普通成員。運行時的主類爲InitialOrderWithExtend類,main()方法中創建了一個子類的對象,並且使用Father對象指向Son類實例的引用(父類對象指向子類引用,多態)。
代碼2:
package com.j2se;
public class InitialOrderWithExtend {
public static void main(String[] args) {
Father ts = new Son();
}
}
class Father {
{
System.out.println("父類 非靜態塊 1 執行");
}
static {
System.out.println("父類 static塊 1 執行");
}
static Sample staticSam1 = new Sample("父類 靜態成員 staticSam1 初始化");
Sample sam1 = new Sample("父類 普通成員 sam1 初始化");
static Sample staticSam2 = new Sample("父類 靜態成員 staticSam2 初始化");
static {
System.out.println("父類 static塊 2 執行");
}
Father() {
System.out.println("父類 默認構造函數被調用");
}
Sample sam2 = new Sample("父類 普通成員 sam2 初始化");
{
System.out.println("父類 非靜態塊 2 執行");
}
}
class Son extends Father {
{
System.out.println("子類 非靜態塊 1 執行");
}
static Sample staticSamSub1 = new Sample("子類 靜態成員 staticSamSub1 初始化");
Son() {
System.out.println("子類 默認構造函數被調用");
}
Sample sam1 = new Sample("子類 普通成員 sam1 初始化");
static Sample staticSamSub2 = new Sample("子類 靜態成員 staticSamSub2 初始化");
static {
System.out.println("子類 static塊1 執行");
}
Sample sam2 = new Sample("子類 普通成員 sam2 初始化");
{
System.out.println("子類 非靜態塊 2 執行");
}
static {
System.out.println("子類 static塊2 執行");
}
}
class Sample {
Sample(String s) {
System.out.println(s);
}
Sample() {
System.out.println("Sample默認構造函數被調用");
}
}
運行結果:
父類 static塊 1 執行
父類 靜態成員 staticSam1 初始化
父類 靜態成員 staticSam2 初始化
父類 static塊 2 執行
子類 靜態成員 staticSamSub1 初始化
子類 靜態成員 staticSamSub2 初始化
子類 static塊1 執行
子類 static塊2 執行
父類 非靜態塊 1 執行
父類 普通成員 sam1 初始化
父類 普通成員 sam2 初始化
父類 非靜態塊 2 執行
父類 默認構造函數被調用
子類 非靜態塊 1 執行
子類 普通成員 sam1 初始化
子類 普通成員 sam2 初始化
子類 非靜態塊 2 執行
子類 默認構造函數被調用
由輸出結果可知,執行的順序爲:
- 父類靜態代碼區和父類靜態成員
- 子類靜態代碼區和子類靜態成員
- 父類非靜態代碼區和普通成員
- 父類構造函數
- 子類非靜態代碼區和普通成員
- 子類構造函數
與非繼承關係中的初始化順序一致的地方在於,靜態代碼區和父類靜態成員、非靜態代碼區和普通成員是同一級別的,當存在多個這樣的代碼塊或者成員時,初始化的順序和它們在程序中申明的順序一致;此外,靜態代碼區和靜態成員也是僅僅初始化一次,但是在初始化過程中,可以修改靜態成員的引用。
初始化順序圖示
非繼承關係
繼承關係
類初始化順序的JVM解釋
類初始化順序受到JVM類加載機制的控制,類加載機制包括加載、驗證、準備、解析、初始化等步驟。不管是在繼承還是非繼承關係中,類的初始化順序主要受到JVM類加載時機、解析和clinit()初始化規則的影響。
加載時機
加載是類加載機制的第一個階段,只有在5種主動引用的情況下,纔會觸發類的加載,而在其他被動引用的情況下並不會觸發類的加載。關於類加載時機和5中主動引用和被動引用詳見【深入理解JVM】:類加載機制。其中3種主動引用的形式爲:
- 程序啓動需要觸發main方法的時候,虛擬機會先觸發這個類的初始化
- 使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、JIT時放入常量池的靜態字段除外)、調用一個類的靜態方法,會觸發初始化
- 當初始化一個類的時候,如果其父類沒有初始化,則需要先觸發其父類的初始化
代碼1中觸發main()方法前,需要觸發主類InitialOrderWithoutExtend的初始化,主類初始化觸發後,對靜態代碼區和靜態成員進行初始化後,打印”第1個主類對象:”,之後遇到newInitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();
,再進行其他普通變量的初始化。
代碼2是繼承關係,在子類初始化前,必須先觸發父類的初始化。
類解析在繼承關係中的自下而上遞歸
類加載機制的解析階段將常量池中的符號引用替換爲直接引用,主要針對的是類或者接口、字段、類方法、方法類型、方法句柄和調用點限定符7類符號引用。關於類的解析過程詳見【深入理解JVM】:類加載機制。
而在字段解析、類方法解析、方法類型解析中,均遵循繼承關係中自下而上遞歸搜索解析的規則,由於遞歸的特性(即數據結構中棧的“後進先出”),初始化的過程則是由上而下、從父類到子類的初始化順序。
初始化clinit()方法
初始化階段是執行類構造器方法clinit() 的過程。clinit() 是編譯器自動收集類中所有類變量(靜態變量)的賦值動作和靜態語句塊合併生成的。編譯器收集的順序是由語句在源文件中出現的順序決定的。JVM會保證在子類的clinit() 方法執行之前,父類的clinit() 方法已經執行完畢。
因此所有的初始化過程中clinit()方法,保證了靜態變量和靜態語句塊總是最先初始化的,並且一定是先執行父類clinit(),在執行子類的clinit()。
代碼順序與對象內存佈局
在前面的分析中我們看到,類的初始化具有相對固定的順序:靜態代碼區和靜態變量先於非靜態代碼區和普通成員,先於構造函數。在相同級別的初始化過程中,初始化順序與變量定義在程序的中順序是一致的。
而代碼順序在對象內存佈局中同樣有影響。(關於JVM對象內存佈局詳見【深入理解JVM】:Java對象的創建、內存佈局、訪問定位。)
在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。而實例數據是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內容。
無論是從父類繼承還是子類定義的,都需要記錄下來,這部分的存儲順序JVM參數和字段在程序源碼中定義順序的影響。HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans、oop,從分配策略中可以看出,相同寬度的字段總是分配到一起。滿足這個條件的前提下,父類中定義的變量會出現在子類之前。不過,如果啓用了JVM參數CompactFields(默認爲true,啓用),那麼子類中較窄的變量也可能會插入到父類變量的空隙中。