1. 程式人生 > >JVM類載入機制--概述&時機&初始化

JVM類載入機制--概述&時機&初始化

概述

虛擬機器的類載入機制:

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。

類是在執行期間第一次使用時動態載入的,而不是編譯時期一次性載入。因為如果在編譯時期一次性載入,那麼會佔用很多的記憶體。

在Java語言裡,型別的載入、連線和初始化過程都是在程式執行期間完成的,這種策略雖然會使類載入時稍微增加一些效能開銷,但是會為Java應用程式提供高度的靈活性,Java裡天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連線這個特點實現的。

類載入時機

類從被載入到虛擬機器記憶體開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連線(Linking),這7個階段發生的順序如圖所示。

類的生命週期

圖中,載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。

Java虛擬機器規範中並沒有強制約束載入階段,這點可以交給虛擬機器的具體實現來自由把握。

對於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行”初始化“(而載入、驗證、準備自然需要在此之前開始):

  1. 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器先會初始化這個主類。
  5. 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方法都不會觸發初始化,稱為被動引用

被動引用舉例:

  • 通過子類引用父類的靜態欄位,不會導致子類初始化。
System.out.println(SubClass.value);  // value 欄位在 SuperClass 中定義
  • 通過陣列定義來引用類,不會觸發此類的初始化。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

介面的載入過程與類載入過程稍有一些不同,特殊說明:

類一般使用靜態語句塊”static{}”來輸出初始化資訊的,而介面中不能使用”static{}”語句塊,但編譯器仍然會為介面生成“<clinit>()”類構造器,用於初始化介面所定義的成員變數。介面與類初始化的真正區別(“有且只有”第3種)

當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如引用介面中定義的常量)才會初始化。

參考自:《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第2版)》周志明