天天看點

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

概述

虛拟機的類加載機制:

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

類是在運作期間第一次使用時動态加載的,而不是編譯時期一次性加載。因為如果在編譯時期一次性加載,那麼會占用很多的記憶體。

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

類加載時機

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

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

類的生命周期

圖中,加載、驗證、準備、初始化和解除安裝這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版)》周志明