深入理解JVM - 類加載過程

類加載過程

Class文件被加載到內存,並對數據進行校驗、轉換解析和初始化,最終卸載出內存,這個過程被稱作類加載過程。類加載過程包括加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個階段統稱爲連接。

加載

加載類加載過程的第一個階段。在加載階段,虛擬機要完成以下3件事情:

  • 通過一個類的全限定名來獲取該類的二進制字節流。
  • 將靜態的字節流轉化爲方法區運行時數據結構。
  • 在Java堆中生成一個java.lang.Class對象,作爲訪問方法區各種數據的入口。

驗證

驗證是指對Class文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。

  • 文件格式驗證是基於字節流的驗證。驗證字節流符合當前的Class文件格式的規範,能被當前虛擬機處理。驗證通過後,字節流纔會進入內存的方法區進行存儲。
  • 元數據驗證是基於方法區的存儲結構驗證。對字節碼進行語義驗證,確保不存在不符合Java語言規範的元數據信息。
  • 字節碼驗證是基於方法區的存儲結構驗證,通過對數據流和控制流的分析,保證被檢驗類的方法在運行時不會做出危害虛擬機的動作。
  • 符號引用驗證是基於方法區的存儲結構驗證,發生在解析階段,確保能夠將符號引用成功的解析爲直接引用,其目的是確保解析動作正常執行。換句話說就是對類自身以外的信息進行匹配性校驗。

準備

準備是正式爲類變量(靜態變量,被static修飾的變量)分配內存並設置類變量的初始值。這些變量所使用的內存都將在方法區中進行分配(JDK 8及之後,類變量則會隨着Class對象一起存放在Java堆中)。這個階段要注意三點:

  • 這個時候進行內存分配的僅包括類變量(靜態變量,被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。
  • 這裏所說的初始值"通常情況"下是數據類型的零值,假設一個類變量的定義爲:
public static int value = 123;

變量value在準備階段會賦值爲0而不是123。在初始化階段調用類構造器時,纔會賦值爲123。

  • 初始值"通常情況"下是數據類型的零值,也就是說還有特殊情況。被static final修飾的常量對value直接賦值爲123。

解析

解析是將符號引用轉變爲直接引用,直接引用實際上是指向了真實的內存地址。解析包括:類或接口的解析,字段解析,類方法解析,接口方法解析。
比如在D類中引用了C類。在編譯階段,D類並不知道C類的實際內存地址,因此只能用C類的符號引用來代替。在解析階段,JVM可以通過解析C類的符號引用,來確定C類的真實內存地址(如果該類未被加載過,則先加載)。

初始化

初始化實際上是調用類構造器<clinit>()。在編譯階段,所有的類變量、靜態代碼塊都會收斂到類構造器中,類初始化階段所有的靜態變量會被重新賦值, 並且執行靜態塊。
類初始化時機,Java虛擬機規範嚴格的規定了有且只有6種情況必須立即對類進行"初始化"。

  • 遇到new、getstatic、setstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態子段(被final修飾、已在編譯期把結果放入常量池的靜態子段除外)的時候,以及調用一個類的靜態方法的時機。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先觸發其初始化。
  • 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章