首先根據下面的這個一段代碼:引入關於java初始化順序的問題
public class InitationTest extends Person { public InitationTest() { System.out.println("InitationTest constructor"); } static int j = 10; static { System.out.println("j="+j); System.out.println("InitationTest static..."); } public static void main(String[] args) { InitationTest obj = new InitationTest(); } } class Person { static { // System.out.println("i="+i); Cannot reference a field before it is // defined i = 10; System.out.println("Person static..."); } static int i = 5; Person() { System.out.println("Person constructor"); } }
打印依次為:
Person static...
j=10
InitationTest static...
Person constructor
InitationTest constructor
想要理解類中初始化順序,就必須先理解jvm加載原理
一:jvm加載順序和原理
類的初始化順序有點類似jvm中類加載器的模式:(雙親委派模型)的工作過程為:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啟動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索范圍中沒有找到對應的類)時,子加載器才會嘗試自己去加載。都是從父類中開始,當然類加載器並不是繼承關系而是組合關系.
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的生命周期包括了:加載(Loading)、驗證(Verification)、准備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)七個階段,其中驗證、准備、解析三個部分統稱鏈接

1.加載:
1.通過“類全名”來獲取定義此類的二進制字節流
2.將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
3.在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口
2.驗證:
1.文件格式驗證
2.元數據驗證
3.字節碼驗證
4.符號引用驗證
3.准備:
准備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點,首先是這時候進行內存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在java堆中。其次是這里所說的初始值“通常情況”下是數據類型的零值,假設一個類變量定義為:
public static int value = 12;
那么變量value在准備階段過后的初始值為0而不是12,因為這時候尚未開始執行任何java方法,而把value賦值為12的putstatic指令是程序被編譯后,存放於類構造器<clinit>()方法之中,所以把value賦值為12的動作將在初始化階段才會被執行。
上面所說的“通常情況”下初始值是零值,那相對於一些特殊的情況,如果類字段的字段屬性表中存在ConstantValue屬性,那在准備階段變量value就會被初始化為ConstantValue屬性所指定的值,建設上面類變量value定義為:
public static final int value = 123;
編譯時javac將會為value生成ConstantValue屬性,在准備階段虛擬機就會根據ConstantValue的設置將value設置為123
4.解析:
解析階段是虛擬機常量池內的符號引用替換為直接引用的過程
5.初始化:
類的初始化階段是類加載過程的最后一步,在准備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計划去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。在以下四種情況下初始化過程會被觸發執行:
1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需先觸發其初始化。生成這4條指令的最常見的java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。
2.使用java.lang.reflect包的方法對類進行反射調用的時候
3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先出發其父類的初始化
4.jvm啟動時,用戶指定一個執行的主類(包含main方法的那個類),虛擬機會先初始化這個類
在上面准備階段 public static int value = 12; 在准備階段完成后 value的值為0,而在初始化階調用了類構造器<clinit>()方法,這個階段完成后value的值為12。
*類構造器<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合並產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句快可以賦值,但是不能訪問。
二:加載順序:
(1)靜態內容:靜態的成員變量、靜態代碼塊、靜態的成員方法按順序加載。
(2)非靜態內容:成員變量、代碼塊、成員方法按順序加載。
總順序:靜態內容--》非靜態內容--》類構造方法
編譯器生成的class文件主要對定義在源文件中的類進行了如下的更改:
1) 先按照(靜態or非靜態)成員變量的定義順序在類內部聲明成員變量。
2) 再按照原java類中對(靜態or非靜態)成員變量的初始化順序進行初始化。
左圖是源文件,右圖類似編譯后的文件:

