面試官,不要再問我「Java虛擬機類載入機制」了
- 2019 年 10 月 27 日
- 筆記
關於Java虛擬機類載入機制往往有兩方面的面試題:根據程式判斷輸出結果和講講虛擬機類載入機制的流程。其實這兩類題本質上都是考察面試者對Java虛擬機類載入機制的了解。
面試題試水
現在有這樣一道判斷程式輸出結果的面試題,先看看列印的結果是什麼?
public class SuperClass { static { System.out.println("SuperClass static init"); } public static String ABC = "abc"; } public class SubClass extends SuperClass{ static { System.out.println("SuperClass static init"); } } public class Main { public static void main(String[] args) { System.out.println(SubClass.ABC); } } 上面定義了三個類,其中SubClass繼承SuperClass,然後Mian類中列印SubClass.ABC的值。那麼,控制台列印結果是什麼?
SuperClass static init abc 你做對了么?這是為什麼呢?對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
再對上面的程式碼進行調整,對靜態變數ABC添加final修飾。
public class SuperClass { static { System.out.println("SuperClass static init"); } public static String ABC = "abc"; } public class SubClass extends SuperClass{ static { System.out.println("SuperClass static init"); } } public class Main { public static void main(String[] args) { System.out.println(SubClass.ABC); } } 列印結果為:
abc 這又是為什麼呢?因為,常量在編譯階段會存入調用類的常量池中,也就是說Main類對SubClass.ABC的引用已經與SuperClass無關了,實際上已經轉行為Main類對ABC的引用了。
做好的鋪墊,可以開始對類載入機制的了解了。
類載入過程
虛擬機把描述類的數據從Class文件載入到記憶體,並對數據進行校驗、轉化解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類載入機制。
整個生命周期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱為連接(Linking)。

其中載入、驗證、準備、初始化和卸載的執行順序是確定的,解析階段則在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。
載入階段
在載入階段虛擬機會完成三件事:
- 通過一個類的全限定名來獲取定義此類的二進位位元組流;
- 將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
- 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口;
其中獲取二進位位元組流可以通過Class文件、ZIP包、網路、運行時(動態代理)、JSP生成、資料庫等途徑獲取。
需要注意的是數組類的載入,數組類並不通過類載入器載入,而是由Java虛擬機直接創建,但數組類的元素還是要依靠類載入器進行載入。
這些二進位位元組流載入完成之後,按照指定的格式存放于于方法區內(Java7及以前方法區位於永久代,Java8位於Metaspace)。然後在方法區生成一個比較特殊的java.lang.Class對象,用來作為程式訪問方法區中這些類型數據的外部介面。
驗證階段
驗證的目的是為了確保Class文件的位元組流中包含的資訊符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

文件格式驗證:驗證位元組流是否符合Class文件格式的規範;比如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支援的類型。只有驗證通過才會進入方法區進行存儲。
元數據驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求;比如,是否有父類(除Object類)、父類是否為final修飾、是否實現抽象方法或介面、重載是否正確等。
位元組碼驗證:通過數據流和控制流分析,確定程式語義是合法的、符合邏輯的。比如,保證數據類型與指令正常配合工作、指令不會跳轉到方法體外的位元組碼上,方法體中的類型轉換是有效的等。
符號引用驗證:在虛擬機將符號引用轉化為直接引用的時候進行驗證,可以看做是對類自身以外的資訊(常量池中的各種符號引用)進行匹配性的校驗。常見的異常比如:java.lang.NoSuchMethdError、java.lang.NoSuchFiledError等。
準備階段
準備階段主要是正式為類變數分配記憶體並設置類變數初始值,變數所使用的記憶體都將在方法區中進行分配。
此處的類變數指的是被static修飾的變數,不包含實例變數,實例變數在對象實例化階段分配在堆中。
public static String ABC = "abc"; 並且,變數的初始化值並不是類中定義的值,而是該變數所屬類型的默認值。

當然,也有特殊情況,比如當變數被final修飾時:
public static final String ABC = "abc"; 此時,該欄位屬性是ConstantValue時,會在準備階段初始化為指定的值。
解析階段
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
這裡我們看一下欄位解析,也就是最開始第一道面試題。當獲取SubClass的屬性ABC時,首先會查找SubClass本身是否包含該欄位,如果包含則直接返回引用,查找結束。
否則,如果SubClass類實現了介面或繼承了父類,那麼則遞歸搜索各個介面和父類,找到匹配的屬性則返回,查找結束。
否則,查找失敗,拋出java.lang.NoSuchFieldError異常。如果返回成功了,但是是許可權校驗失敗,也就是無該欄位的訪問許可權,則拋出java.lang.IllegalAccessError異常。
其他形式的解析,就不再這裡一一說明了。
初始化階段
初始化階段才是真正執行類中定義的Java程式程式碼(位元組碼)。在此階段會根據程式碼進行類變數和其他資源的初始化,或者可以從另一個角度來表達:初始化階段是執行類構造器
編譯器提示錯誤。

將其放在後面,則正常編譯執行,輸出結果為「edf」:

如果將static中的列印語句去掉,那麼下面這段程式碼的列印結果會是什麼呢?
public class Main { static { //可以賦值 abc = "edf"; //編譯器會提示「非法向前引用」 // System.out.println(abc); } static String abc = "abc"; public static void main(String[] args) { System.out.println(abc); } } 列印結果為「abc」。在準備階段屬性abc的值為null,然後類初始化按照順序執行,首先執行static塊中的abc=「edf」賦值操作,接著執行abc="abc"的賦值操作,此時值為「abc」。當main方法調用列印時則為「abc」。
由於父類的
介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成
虛擬機會保證一個類的
虛擬機規範初始化
虛擬機規範嚴格規定了有且只有5中情況(jdk1.7)必須對類進行「初始化」(而載入、驗證、準備自然需要在此之前開始):
- 遇到new,getstatic,putstatic,invokestatic這失調位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用jdk1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
該段內容引自周志明《深入理解java虛擬機》。
小結
經過以上步驟,便完成了虛擬機類的載入過程,後續會繼續講解虛擬機的類載入器和雙親委派機制。歡迎大家關注公眾號「程式新視界」繼續深入學習。
原文鏈接:《面試官,不要再問我「Java虛擬機類載入機制」了》
《面試官》系列文章:


