JVM系列:(六)JVM類載入步驟
原文連結: JVM系列:(六)JVM類載入步驟
一 什麼是類載入
上一章我們瞭解了class檔案儲存結構,在class檔案中描述的各種資訊,最終都需要載入到虛擬機器中之後才能被執行和使用。而虛擬機器是如何載入這些class檔案的?
虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。
JVM中型別的載入和連線過程都是在程式執行期間完成的,這樣會在類載入時稍微增加一些效能開銷,但是卻能為Java應用程式提供高度的靈活性,Java中天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連線這個特性實現的。例如,如果編寫一個使用介面的應用程式,可以等到執行時再指定其實際的實現。
類的完整生命週期包括:載入(loading)、驗證(verification)、準備(preparation)、解析(resolution)、初始化(initialization)、使用(using)、解除安裝(unloading)。其中驗證、準備、解析這三個步驟可以統稱為連線(linking)。如下圖所示:

類的生命週期
今天我們要說的類載入步驟不包括最後兩步的使用和解除安裝,主要講解步驟為: 載入、驗證、準備、解析和初始化 。
二 類載入步驟
2.1 載入
載入階段是虛擬機器類載入的第一步驟,在載入階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在Java堆記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這些資料的訪問入口。
虛擬機器規範的這三點要求實際上並不具體,例如“通過一個類的全限定名來獲取定義此類的二進位制位元組流”,並沒有指明二進位制位元組流要從一個 class 檔案中獲取,沒有指明要從哪裡獲取及怎樣獲取,例如:
- 從 ZIP 包中讀取,這很常見,最終成為日後JAR、EAR、WAR 格式的基礎。
- 從網路中獲取,這種場景最典型的應用就是 Applet。
- 執行時計算生成,這種場景使用得最多的就是動態代理技術。
- 由其他檔案生成,典型場景:JSP應用。
- 從資料庫中讀取,這種場景相對少見。
- ......
相對於類載入過程的其他階段,載入階段是開發期可控性最強的階段,載入階段既可以使用系統提供的類載入器來完成,也可以由使用者自定義的類載入器去完成,開發人員可以定義自己的類載入器去控制位元組流的獲取方式。
載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區中,然後在Java堆記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區中的這些型別資料的外部介面。
2.2 驗證
這一階段的目的是為了確保class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。我們前面已經說過,class檔案不一定要求用Java原始碼編譯而來,可以使用任何途徑,包括用十六進位制編輯器直接編寫產生class檔案。虛擬機器如果不檢查輸入的位元組流,對其完全信任的話,很可能會因為載入了有害的位元組流而導致系統崩潰,所以驗證是虛擬機器對自身保護的一項重要工作。
大致上驗證會完成下面四個階段的檢驗過程:
- 檔案格式驗證:驗證是否符合class檔案格式的規範,並且能被當前版本的虛擬機器處理。主要驗證點就是我們上一章所說class檔案結構的一些資訊。這一階段的驗證是基於位元組流進行的,經過了這個階段的驗證之後,位元組流才會進入記憶體的方法區中進行儲存,所以後面的三個驗證階段全部是基於方法區的儲存結構進行的。
- 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。例如是否有父類、父類是否被final修飾、類中的欄位,方法是否與父類產生矛盾等。主要驗證目的是對類的元資料資訊進行語義校驗,保證不存在不符合Java語言規範的元資料資訊。
- 位元組碼驗證:主要進行資料流和控制流分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為。例如保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作、保證跳轉指令不會跳轉到方法體外的位元組碼指令上、保證方法體中的型別轉換是有效的等。
- 符號引用驗證:對類自身以外(常量池中的各種符號引用)的資訊進行匹配性的校驗,通常需要校驗的內容有符號引用中通過字串描述的全限定名是否能找到對應的類、在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位、符號引用中的類、欄位和方法的訪問性(private、protected、public、default)是否可被當前類訪問等。
2.3 準備
準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。
這個階段中需要注意的兩點:
- 這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不是例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆記憶體中。
- 這裡所說的初始值“通常情況”下是資料型別的零值。例如int型別的初始化是0、long型別的初始值是0L等。 真正給類變數賦定義值的動作將在下面的初始化階段才會被執行 。
2.4 解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。包括:
- 類或介面的解析
- 欄位解析
- 類方法解析
- 介面方法解析
2.5 初始化
類初始化階段是類載入過程的最後一步,前面的類載入過程中,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼(編譯後的位元組碼)。
在準備階段,變數已經被賦過一次變數預設的初始值,而在初始化階段,則會根據程式中編寫的具體值去初始化類變數和其他資源。
對於初始化階段,虛擬機器規範嚴格規定了有且只有四種情況必須立即對類進行“初始化”(載入、驗證、準備自然需要在此之前開始);
- 遇到new、getstatic、putstatic 或 invokestatic 這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條只能的最常見Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
- 使用 java.lang.reflect 包的方法對類進行發射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
有且僅有以上四種情況會執行類的初始化。
三 總結
類的載入步驟總結來說就是,先把遠端或磁碟的class檔案載入到記憶體中,並在Java堆記憶體生成該類的class物件以供外部訪問、再對載入到的資料進行驗證,校驗是否符合JVM定義的class檔案結構規範、其次再對定義的類變數賦初始值、最後就是對類變數進行賦定義值以及相關初始化工作。
接下來我們將介紹:
- 類載入器

掃碼關注有驚喜