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
*///:~
我們看一下上述代碼的執行過程:
- 首先第3行調用StaticTest類的靜態方法,觸發了該類的初始化。
- 類的初始化執行的第一步就是第6行語句。
- 由於第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