從一個例子看Java的數據初始化和類加載

一、代碼鎮帖

package javase.jvm;

public class ClassInitTest {

    private static final String staticCodeBlock = " static code block ";
    private static final String codeBlock = " code block ";
    private static final String constructor = " constructor ";

    private static String className = ClassInitTest.class.getSimpleName();

    static {
        //靜態初始代碼塊,用於驗證主函數類的加載
        System.out.println(className + staticCodeBlock);
    }

    static class Motherland{

        static final String name = "China";

        static {
            System.out.println("Motherland " + staticCodeBlock);
            //對類的靜態變量進行賦值,但是不能使用定義在靜態代碼塊後面的變量
            age = 79;
        }

        //一個靜態字段
        static Motherland motherland = new Motherland();

        //靜態字段
        static int age = 78;
        static int count;

        {
            //構造代碼塊
            System.out.println("Motherland " + codeBlock);
        }

        //私有構造器
        private Motherland(){
            System.out.println("Motherland " + constructor);
            age ++;
            count ++;
        }
    }

    static class Successor extends Motherland{

        static String name = "cyf";

        int count1 = getCount2();

        int count2 = 2;

        static {
            System.out.println("Successor " + staticCodeBlock);
            name = "chuyf";
        }

        {
            System.out.println("Successor " + codeBlock);
            count2 = 0;
        }

        Successor(){
            System.out.println("Successor " + constructor);
        }

        int getCount2(){
            return count2;
        }
    }

    public static void main(String[] args){

        System.out.println("Motherland name: " + Motherland.name);

        System.out.println("Successor name: " + Successor.name);

        System.out.println("Motherland age: " + Motherland.age);

        Successor successor = new Successor();

        System.out.println("successor count1: " + successor.count1 + "\t" + "successor count2: " + successor.count2);

        System.out.println("Motherland age: " + Motherland.age);

        System.out.println("Motherland count: " + Motherland.count);
    }
}

二、代碼輸出

ClassInitTest static code block //主函數類初始化
Motherland name: China //輸出靜態常量
Motherland  static code block //父類靜態代碼塊
Motherland  code block //父類構造代碼塊
Motherland  constructor //父類構造函數
Successor  static code block //子類靜態代碼塊
Successor name: chuyf //輸出
Motherland age: 78 //輸出
Motherland  code block //父類構造代碼塊
Motherland  constructor //父類構造函數
Successor  code block //子類構造代碼塊
Successor  constructor //子類構造函數
successor count1: 0	successor count2: 0 //輸出對象值
Motherland age: 79 //輸出
Motherland count: 2 //輸出

三、輸出分析

  1. 當虛擬機啓動時,虛擬機會先初始化要執行的主類(包含main函數的類)。

第一個輸出表示當前主類已經被加載。

  1. 當實例化對象、讀取或設置類的靜態字段(final修飾的常量除外)、調用類靜態方法時將觸發類的加載。

第75行輸出父類的靜態字段,但是並沒有引起父類的加載。在編譯時,常量傳播將該常量直接放置在主類的常量池裏面,於是對父類靜態字段的引用變成了對自己類常量池的一個引用。使用javap -c -v ClassInitTest.class 命令,可以在Constant pool裏面找到這個常量#60 = Utf8 Motherland name: China,所以第75行沒有引起其他的類加載(主類已經被加載了)

  1. 當初始化一個類時,如果發現其父類沒有進行初始化,則先觸發其父類的初始化(所以Object類肯定最先被初始化)。

在第77行訪問子類的時候將觸發其進行類加載,然後觸發其父類的加載,所以輸出的爲:父類靜態代碼塊(第三行輸出) > 子類靜態代碼塊(第六行輸出)的大致順序。之間輸出了其他的信息先不管

  1. 虛擬機類加載機制

類的生命週期主要有以下幾個階段:加載、驗證、準備、解析、初始化、使用、卸載 七個階段。除了解析階段爲了支持動態語言可以在初始化後開始,其他的階段都是順序開始,交叉進行的。

加載:通過全限定類名獲取一個字節流,將其靜態存儲結構轉換爲運行時數據結構,並創建一個代表該類的Class對象作爲訪問該類信息的入口。

驗證:確保Class文件裏面的字節流不會危害虛擬機本身。包含文件格式驗證(是否符合Class文件格式規範、是否被當前虛擬機支持)、元數據驗證(字節碼描述信息語義分析)、字節碼驗證(程序語義是否合法、符合邏輯)、符號引用驗證(自身信息的匹配驗證)。

準備:爲類變量分配空間並設置類變量的初始值。常量將被直接賦值爲常量值,而不是初始值。

解析:將符號引用解析爲直接引用的過程(符號上的邏輯關係轉換爲虛擬機內存之間的聯繫)。

初始化:類構造器的執行過程。編譯器順序收集類變量的賦值操作和靜態代碼塊的語句合併而成。

  1. 解釋第77行導致的輸出

第77行對子類靜態字段的讀取引發對子類的加載,子類引發對父類的加載。

父類加載過程:

  1. 準備階段將常量賦值,將靜態變量賦值爲初始值,準備階段後各個靜態變量的值:name = "China"; motherland = null; age = 0; count = 0;
  2. 初始化階段收集賦值操作和static代碼塊對類變量進行初始化。第一個操作,執行靜態代碼塊(第三行輸出),對age進行賦值操作,該操作後,各個靜態變量的值爲:name = "China"; motherland = null; age = 79; count = 0;。第二個操作,初始化motherland,對motherland的初始化需要進行構造,所以需要依次調用構造代碼塊(第四行輸出)和構造函數(第五行輸出)。第一步過後,各個變量的值:name = "China"; motherland = [object]; age = 80; count = 1;第三個操作,執行對age的賦值操作,改操作後,各個靜態變量的值爲:name = "China"; motherland = [object]; age = 78; count = 1;至此父類加載初始化完畢。

子類加載過程:

  1. 準備階段類變量:name = null;
  2. 初始化階段,48行將其賦值爲:“cyf”,但是在緊隨其後的靜態代碼塊(第六行輸出)將其修改爲:“chuyf”。至此子類初始化完成。
  1. 第79行不會導致類加載,因爲都已經被加載過了。
  2. 第81行導致的輸出解釋

對父類的影響:

  1. 第81行對對象的實例化會調用父類的構造代碼塊(第九行輸出)和構造函數(第十行輸出),會將age變爲79(第十四行輸出),count變爲2(第十五行輸出)。

對子類的影響:

  1. Java在進行對象創建時:檢查是否已經類加載、分配內存、初始化爲零值、對象元數據設置、初始化。該類的函數的字節碼如下:
    在這裏插入圖片描述
    反編譯的代碼如下:
    在這裏插入圖片描述
    至此,類、實例的實例化順序複習完成。

四、初始化順序總結

  1. 父類類構造函數(順序的靜態字段賦值語句與static代碼塊)
  2. 子類類構造函數(順序的靜態字段賦值語句與static代碼塊)
  3. 父類的構造代碼塊和構造函數
  4. 父子類類的構造代碼塊和構造函數

注:

  1. 在進行初始化時,常量在準備階段就已經賦值,而類靜態變量爲零值。
  2. 當執行順序前的初始化操作去使用後續未初始化的值時將會訪問到零值(準備階段),常量除外。
  3. 常量在編譯階段的傳播優化不會導致該常量聲明類的加載。
  4. 類構造是線程安全的,所以注意類構造的時間。
  5. 最好的方法是看字節碼文件,實例變量賦值、構造代碼塊和構造函數被整合到<init>函數,靜態代碼塊和靜態變量賦值被整合到<cinit>函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章