JVM類生命週期與類的加載過程

JVM類生命週期與類的加載過程

類的生命週期:

在這裏插入圖片描述

.class文件被加載到虛擬機內存後纔可生效。

類加載過程嚴格按照上述順序“開始”,但不是按照上述順序“進行”或“完成”,可能會交錯。

1、關於類初始化時機

當且僅當對一個類進行主動引用的時候纔會觸發初始化階段。共有5種主動引用場景,詳見文末參考文章。

其餘被動引用不觸發類的初始化,如:

1) 通過子類引用父類的靜態字段,不會導致子類的初始化。

2)通過數組定義來引用類,不會觸發該類的初始化。如:

public class NotInitialization{
    public static void main(String[] args){
        SClass[] sca = new SClass[10];
    }
}

虛擬機並沒有初始化Sclass, 而是觸發了一個[Lcn.edu.tju.rico.SClass類的初始化,其中"["代表數組,該類直接繼承於Object,創建動作由字節碼指令newarray觸發。

3)常量在編譯階段會存入調用類的常量池中,不會觸發定義該常量的類的初始化。如:

// 定義類
public class ConstClass{

    static{
        System.out.println("ConstClass init!");
    }

    public static  final String CONSTANT = "hello world";
}
// 調用類
public class NotInitialization{
    public static void main(String[] args){
        System.out.println(ConstClass.CONSTANT);
    }
}/* Output: 
        hello world
 *///:~

2、進一步解釋生命週期的每一步

1)加載

參加文末參考文章

2)驗證

cafebabe,參見文末參考文章

3)準備階段

對類的static變量的內存分配(jdk1.8: 元空間直接內存中), 與第一次初始化(零值)。

注意:若是final 的static變量,則初始化爲定義時所賦的值。

4)解析

參加文末參考文章

5)初始化階段

第二次對類變量初始化,這一次是按照程序員的意志。

執行類構造器<clinit>()方法,整合,合併所有類變量的賦值動作靜態語句塊,類似於對象創建(類的實例化)中的實例構造器<init>()

注意:靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問。如下:

public class Test{
    static{
        i=0;
        System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}

那麼註釋報錯的那行代碼,改成下面情形,程序就可以編譯通過並可以正常運行了:

public class Test{
    static{
        i=0;
        //System.out.println(i);
    }

    static int i=1;

    public static void main(String args[]){
        System.out.println(i);
    }
}/* Output: 
        1
 *///:~

虛擬機會保證在子類類構造器<clinit>()執行之前,父類的類構造<clinit>()執行完畢。

虛擬機會保證一個類的類構造器在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的類構造器,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。且,在同一個類加載器下,一個類型只會被初始化一次

3、小結

結合java對象的創建,我們可以歸納,創建一個java對象需要經歷如下幾個階段:

父類的類構造器() -> 子類的類構造器() -> 父類的成員變量和實例代碼塊 -> 父類的構造函數 -> 子類的成員變量和實例代碼塊 -> 子類的構造函數。

4、案例分析(類的初始化與實例化的糾結)

public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {   //靜態代碼塊
        System.out.println("1");
    }

    {       // 實例代碼塊
        System.out.println("2");
    }

    StaticTest() {    // 構造函數
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {   // 靜態方法
        System.out.println("4");
    }

    int a = 110;    // 實例變量/成員變量
    static int b = 112;     // 靜態變量
}
/* Output: 
        2
        3
        a=110,b=0 // 因爲先進行了實例化,所以這裏b是“準備”階段的0值。
        1
        4
 *///:~

我們看一下上述代碼的執行過程:

  1. 首先第3行調用StaticTest類的靜態方法,觸發了該類的初始化
  2. 類的初始化執行的第一步就是第6行語句。
  3. 由於第6行是==類的實例化,也就是說,這裏將類的實例話嵌入到了類的加載過程之中,在準備階段之後,類的初始化之前。==類似這樣:
public class StaticTest {
    <clinit>(){
        a = 110;    // 實例變量
        System.out.println("2");        // 實例代碼塊
        System.out.println("3");     // 構造函數中代碼的執行
        System.out.println("a=" + a + ",b=" + b);  // 構造函數中代碼的執行
        類變量st被初始化
        System.out.println("1");        //靜態代碼塊
        類變量b被初始化爲112
    }
}

所以就有了上述的輸出。

結論:實例初始化不一定要在類初始化結束之後纔開始初始化。

參考文章:https://blog.csdn.net/justloveyou_/article/details/72466105

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章