Java類繼承關係中的初始化順序

來源:互聯網
上載者:User

標籤:載入   blog   msu   必須   特性   comm   控制代碼   動作   出現   

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成員變數

 

由輸出結果可知,執行順序為:

  1. static靜態代碼塊和靜態成員
  2. 普通成員
  3. 建構函式執行

當具有多個靜態成員和靜態代碼塊或者多個普通成員時,初始化順序和成員在程式中申明的順序一致。

注意到在該程式的靜態代碼塊中,修改了靜態成員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  執行子類 預設建構函式被調用

由輸出結果可知,執行的順序為:

  1. 父類靜態代碼區和父類靜態成員
  2. 子類靜態代碼區和子類靜態成員
  3. 父類非靜態代碼區和普通成員
  4. 父類建構函式
  5. 子類非靜態代碼區和普通成員
  6. 子類建構函式

與非繼承關係中的初始化順序一致的地方在於,靜態代碼區和父類靜態成員、非靜態代碼區和普通成員是同一層級的,當存在多個這樣的代碼塊或者成員時,初始化的順序和它們在程式中申明的順序一致;此外,靜態代碼區和靜態成員也是僅僅初始化一次,但是在初始化過程中,可以修改靜態成員的引用。

初始化順序圖示

非繼承關係

繼承關係

類初始化順序的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,啟用),那麼子類中較窄的變數也可能會插入到父類變數的空隙中。

Java類繼承關係中的初始化順序

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.