Java類載入機制總結

不洗碗工作室發表於2018-08-21

作者:不洗碗工作室 - Marklux

出處:Marklux's Pub

版權歸作者所有,轉載請註明出處

本部分整理自《深入理解JVM虛擬機器》

類的生命週期與載入時機

  1. 類的生命週期

    一個類從被載入到虛擬機器記憶體中開始,到被解除安裝出記憶體為止,整個生命週期包括了 載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中 驗證、準備、解析 3部分統稱為連結,如下圖:

    Java類載入機制總結

    整個順序並不是完全固定的,其中解析階段可以在初始化之後再開始,這樣便可以實現Java的執行時繫結(動態繫結)機制。

  2. 類的載入時機

    JVM虛擬機器規範並沒有對類的載入時機做出嚴格的要求,只規定了以下五種情況需要立刻觸發類的初始化:

    • 遇到new,getstatic,putstatic和invokestatic這四個位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
    • 使用反射機制對類進行呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
    • 當初始化一個類時,如果其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
    • 虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main方法),此時會先初始化這個類
    • 使用JDK1.7的動態語言支援時,如果一個MethodHandle例項最後的解析結果包含REF_getStatic,REF_putStatic,REF_invokeStatic的方法控制程式碼,且這個方法控制程式碼對應的類沒有初始化,則需要先對其進行初始化。

    其餘條件下,可以由JVM虛擬機器自行決定何時去載入一個類。

  3. 主動引用和被動引用

    上面五種條件也被稱為對類的主動引用,除此之外其他引用類的方式都不會觸發初始化,即類的被動引用,舉個例子:

    public class Father {
    	static {
    		System.out.println("father init.");
    	}
    	public static int val = 123;
    }
    
    public class Son extends Father {
    	static {
    		System.out.println("son init.");
    	}
    }
    複製程式碼

    當我們訪問Son.val時,會發現並沒有輸出son init.

    對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過子類來引用父類的靜態欄位,子類相當於是被動引用,也就不會被初始化了。

類的載入過程

下面簡單的介紹一下整個載入過程中,每個階段JVM都執行了什麼操作:

載入(Loading)

載入過程是Java的一大特點,類的來源可以多種多樣,壓縮包、網路位元組流、執行時動態計算生成(reflect)等等...這也造就了Java語言強大的動態特性。

  1. 通過一個類的完整限定名來獲取定義此類的二進位制位元組流(注意,位元組流的來源非常靈活)
  2. 將這個位元組流所代表的靜態儲存結構轉換成為方法區的執行時資料結構
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

驗證(Verification)

這一過程主要是為了確保Class的位元組流中包含的資訊符合虛擬機器標準,以免造成破壞

  1. 檔案格式驗證
  2. 後設資料驗證
  3. 位元組碼驗證,通過資料流和控制流分析確定程式的語義是合法的
  4. 符號引用驗證,確保解析動作能夠正常執行

準備(Preparation)

這一階段將會為類變數分配記憶體並設定其初始值,注意此時進行記憶體分配的僅包括類變數(static修飾),並且初始值通常情況下是資料型別的零值而不是設定值,如下例

public static int val = 123;
複製程式碼

在這一階段變數val的賦值是0而不是123,因為此時尚未執行任何Java方法,而對val複製的putstatic指令在初始化階段後才會執行。

當然也有特殊情況,如下

public static final int val = 123;
複製程式碼

加上final關鍵字修飾後,Java編譯時會為val生成ConstantValue屬性,這時準備階段就會根據設定將其值設定為123。

解析(Resolution)

此階段虛擬機器將常量池內的符號替換為直接引用,主要包含以下動作:

  1. 類或介面的解析
  2. 欄位解析
  3. 類方法解析
  4. 介面方法解析

初始化(Initialization)

這時類載入過程的最後一步,這部分開始真正的執行Java程式碼,也就是說,這個階段可以由程式設計師參與。

此階段其實就是執行類構造器<clinit>()方法的過程。

類載入器

類載入器(Class Loader)是Java虛擬機器的一大創舉,它將“獲取類的二進位制位元組流”這個過程交給了開發人員自己去實現,只要編寫不同的Class Loader,應用程式本身就可以用相應的方式來獲取自己需要的類。

類與載入器的關係

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在虛擬機器中的唯一性。

通俗的講,就是即便同一個Class檔案,被不同的類載入器載入之後,得到也不是同一個“類”(equals方法返回false)。

雙親委派模型

從虛擬機器角度講,只有兩種類載入器,一種是啟動類載入器(Bootstrap ClassLoader),在hotpot上使用C++實現,屬於虛擬機器的一部分;另一種則是所有其他類的載入器,這些載入器是獨立於虛擬機器的,由Java語言實現的,從開發者角度看,可以分為以下兩類:

  1. 擴充套件類載入器(Extension ClassLoader)

  2. 應用程式類載入器(Appliaction ClassLoader)

當然開發人員也可以自己編寫類載入器,最終不同的類載入器之間的層次關係如下圖所示:

Java類載入機制總結

這就是Java中著名的雙親委派模型,它要求除了頂級的BootStrap載入器之外,其他類載入器都必須有父類載入器,工作流程如下:

如果一個類載入器收到了類載入的請求,他首先不會自己去嘗試載入這個類,而是將這個請求委派給父類載入器去完成,只有當父載入器反饋自己無法完成載入請求時,子載入器才會自己去嘗試載入這個類。

這樣做的好處是,Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。舉個例子,比如java.lang.Object這個類,無論哪個類載入器載入時,最終都會委派給Bootstrap載入器去載入,這就保證了整個系統執行過程中的Object都是同一個類。

否則,如果使用者自己編寫了一個java.lang.Object類,並放在程式的classpath中,最終系統將會出現多個不同的Object類,整個Java體系就變得一團混亂了。

相關文章