深入理解JVM(六):虛擬機器類載入機制
虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。
在Java中,型別的載入、連線和初始化過程都是程式在執行期間完成的,這種策略雖然會令類載入時稍微增加一些效能開銷,但是會為Java應用程式提供高度的靈活性。Java裡天生的動態擴充套件語言特性就是依賴執行期動態載入和動態連線這個特點實現的。
1. 類載入的時機
從類被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備、解析3個部分統稱為連線。這7個階段發生的順序如圖所示。
載入、驗證、準備、初始化和解除安裝這個5階段的順序是確定,類的載入過程必須按照這種順序按部就班地開始。而解析階段則不一定:它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言執行時繫結(也稱為動態繫結或者晚期繫結)。
注意:這裡是寫的是按部就班的開始,而不是按部就班的“進行或者完成”,強調這點是因為這些階段都是交叉地混合式進行的,通常會在一個階段執行的過程中呼叫、啟用另外一個階段。
1.1 必須對類進行“初始化”的5種情況
什麼情況下開始類載入中的載入過程,虛擬機器並沒有進行強制約束,這點可以交給虛擬機器的具體實現來自由把握, 但是對於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始)。
(1)使用new 關鍵字例項化物件的時候(對應的位元組碼指令是new)、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
(2)使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。
(3)當初始化一個類的時候,如果其父類還沒有初始化,則需要先觸發其父類的初始化。
(4)當虛擬機器啟動時,使用者需要制定一個要執行的主類,虛擬機器會先初始化這個主類。
(5)當使用JDK1.7的動態語言支援的一些情況。
這5種場景中的行為稱之為對一個類進行主動引用。除此之外,所有的引用類的方式都不會觸發初始化,稱為被動引用。下面是三個被動引用的例子:
(1)通過子類引用父類的靜態欄位,不會導致子類初始化。
(2)通過陣列定義引用類(建立陣列的指令不是new),不會觸發此類的初始化。
(3)常量(變數加上final修飾)在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類,因此不會觸發定義常量的類的初始化。
介面的載入過程與類載入的過程稍有一些不同,介面也有初始化過程,這點與類是一致的,上面的程式碼都是用靜態語句塊"static{}"來輸出初始化資訊的,而介面中不能使用“static{}”語句塊,但編譯器仍然會為介面生成“<clinit()>”類構造器,用於初始化介面中所定義的成員變數。介面與類真正有所區別的是前面講述的5種"有且僅有"需要開始初始化場景中的第3種:當一個類再初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化的時候,並不要求其父介面全部完成了初始化,只有在真正使用到父介面的時候才會初始化。
2. 類載入的過程
接下來我們詳細講解一下Java虛擬機器中類載入的全過程,也就是“載入、驗證、準備、解析和初始化”這個5階段。
2.1 載入
在載入階段需要完成的三件事: (1) 通過一個類的全限定名來獲取定義此類的二進位制位元組流
(2)將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構。
(3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。