類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序如所示:
在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的運行時綁定(也成為動態綁定或晚期綁定)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或啟用另一個階段。
假設上面的類變數value被定義為:
中對象被動引用的第2個例子,便是這種情況。
解析階段是虛擬機器將常量池中的符號引用轉化為直接引用的過程。在Class類檔案結構一文中已經比較過了符號引用和直接引用的區別和關聯,這裡不再贅述。前面說解析階段可能開始於初始化之前,也可能在初始化之後開始,虛擬機器會根據需要來判斷,到底是在類被載入器載入時就對常量池中的符號引用進行解析(初始化之前),還是等到一個符號引用將要被使用前才去解析它(初始化之後)。CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。1、類或介面的解析:判斷所要轉化成的直接引用是對數群組類型,還是普通的物件類型的引用,從而進行不同的解析。2、欄位解析:對欄位進行解析時,會先在本類中尋找是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則尋找結束;如果沒有,則會按照繼承關係從上往下遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從上往下遞迴搜尋其父類,直至尋找結束,尋找流程如所示:
class Super{public static int m = 11;static{System.out.println("執行了super類靜態語句塊");}}class Father extends Super{public static int m = 33;static{System.out.println("執行了父類靜態語句塊");}}class Child extends Father{static{System.out.println("執行了子類靜態語句塊");}}public class StaticTest{public static void main(String[] args){System.out.println(Child.m);}}
執行結果如下: 執行了父類靜態語句塊
33
如果注釋掉Father類中對m定義的那一行,則輸出結果如下: 11另外,很明顯這就是
上篇博文中的第1個例子的情況,這裡我們便可以分析如下:static變數發生在靜態解析階段,也即是初始化之前,此時已經將欄位的符號引用轉化為了記憶體引用,也便將它與對應的類關聯在了一起,由於在子類中沒有尋找到與m相匹配的欄位,那麼m便不會與子類關聯在一起,因此並不會觸發子類的初始化。 如果對上面的代碼做些修改,將Super改為介面,並將Child類繼承Father類且實現Super介面,那麼在編譯時間會報出如下錯誤:都匹配
System.out.println(Child.m);
^
1 錯誤
3、類方法解析:對類方法的解析與對欄位解析的搜尋步驟差不多,只是多了判斷該方法所處的是類還是介面的步驟,而且對類方法的匹配搜尋,是先搜尋父類,再搜尋介面。
4、介面方法解析:與類方法解析步驟類似,知識介面不會有父類,因此,只遞迴向上搜尋父介面就行了。
初始化是類載入過程的最後一步,到了此階段,才真正開始執行類中定義的Java程式碼。在準備階段,類變數已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程式員通過程式指定的主觀計划去初始化類變數和其他資源,或者可以從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。
class Father{public static int a = 1;static{a = 2;}}class Child extends Father{public static int b = a;}public class ClinitTest{public static void main(String[] args){System.out.println(Child.b);}}
很明顯是根據規則1,執行Father的<clinit>()方法時,根據順序先執行了static語句塊中的內容,後執行了“public static int a = 1;”語句。