從一個例子看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 //輸出
三、輸出分析
- 當虛擬機器啟動時,虛擬機器會先初始化要執行的主類(包含main函式的類)。
第一個輸出表示當前主類已經被載入。
- 當例項化物件、讀取或設定類的靜態欄位(final修飾的常量除外)、呼叫類靜態方法時將觸發類的載入。
第75行輸出父類的靜態欄位,但是並沒有引起父類的載入。在編譯時,常量傳播將該常量直接放置在主類的常量池裡面,於是對父類靜態欄位的引用變成了對自己類常量池的一個引用。使用
javap -c -v ClassInitTest.class
命令,可以在Constant pool裡面找到這個常量#60 = Utf8 Motherland name: China
,所以第75行沒有引起其他的類載入(主類已經被載入了)
- 當初始化一個類時,如果發現其父類沒有進行初始化,則先觸發其父類的初始化(所以Object類肯定最先被初始化)。
在第77行訪問子類的時候將觸發其進行類載入,然後觸發其父類的載入,所以輸出的為:父類靜態程式碼塊(第三行輸出) > 子類靜態程式碼塊(第六行輸出)的大致順序。之間輸出了其他的資訊先不管
- 虛擬機器類載入機制
類的生命週期主要有以下幾個階段:載入、驗證、準備、解析、初始化、使用、解除安裝 七個階段。除了解析階段為了支援動態語言可以在初始化後開始,其他的階段都是順序開始,交叉進行的。 載入:通過全限定類名獲取一個位元組流,將其靜態儲存結構轉換為執行時資料結構,並建立一個代表該類的Class物件作為訪問該類資訊的入口。 驗證:確保Class檔案裡面的位元組流不會危害虛擬機器本身。包含檔案格式驗證(是否符合Class檔案格式規範、是否被當前虛擬機器支援)、元資料驗證(位元組碼描述資訊語義分析)、位元組碼驗證(程式語義是否合法、符合邏輯)、符號引用驗證(自身資訊的匹配驗證)。 準備:為類變數分配空間並設定類變數的初始值。常量將被直接賦值為常量值,而不是初始值。 解析:將符號引用解析為直接引用的過程(符號上的邏輯關係轉換為虛擬機器記憶體之間的聯絡)。 初始化:類構造器的執行過程。編譯器順序收集類變數的賦值操作和靜態程式碼塊的語句合併而成。
- 解釋第77行導致的輸出
第77行對子類靜態欄位的讀取引發對子類的載入,子類引發對父類的載入。 父類載入過程:
- 準備階段將常量賦值,將靜態變數賦值為初始值,準備階段後各個靜態變數的值:
name = "China"; motherland = null; age = 0; count = 0;
- 初始化階段收集賦值操作和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;
至此父類載入初始化完畢。子類載入過程:
- 準備階段類變數:
name = null;
- 初始化階段,48行將其賦值為:“cyf”,但是在緊隨其後的靜態程式碼塊(第六行輸出)將其修改為:“chuyf”。至此子類初始化完成。
- 第79行不會導致類載入,因為都已經被載入過了。
- 第81行導致的輸出解釋
對父類的影響:
- 第81行對物件的例項化會呼叫父類的構造程式碼塊(第九行輸出)和建構函式(第十行輸出),會將age變為79(第十四行輸出),count變為2(第十五行輸出)。
對子類的影響:
- Java在進行物件建立時:檢查是否已經類載入、分配記憶體、初始化為零值、物件元資料設定、初始化。該類的函式的位元組碼如下: 反編譯的程式碼如下: 至此,類、例項的例項化順序複習完成。
四、初始化順序總結
- 父類類建構函式(順序的靜態欄位賦值語句與static程式碼塊)
- 子類類建構函式(順序的靜態欄位賦值語句與static程式碼塊)
- 父類的構造程式碼塊和建構函式
- 父子類類的構造程式碼塊和建構函式
注:
- 在進行初始化時,常量在準備階段就已經賦值,而類靜態變數為零值。
- 當執行順序前的初始化操作去使用後續未初始化的值時將會訪問到零值(準備階段),常量除外。
- 常量在編譯階段的傳播優化不會導致該常量宣告類的載入。
- 類構造是執行緒安全的,所以注意類構造的時間。
- 最好的方法是看位元組碼檔案,例項變數賦值、構造程式碼塊和建構函式被整合到<init>函式,靜態程式碼塊和靜態變數賦值被整合到<cinit>函式。