1. 程式人生 > 實用技巧 >【深入理解Java虛擬機器】類載入機制

【深入理解Java虛擬機器】類載入機制

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

類載入的時機

載入階段合適開始,《Java虛擬機器規範》並沒有強制約束,交由虛擬機器自己實現。

而初始化階段,嚴格規定有且只有以下六種情況,如果型別沒有進行過初始化,必須對類進行初始化:

  • 遇到以下位元組碼指令:

    • new:例項化物件的時候
    • getstatic和putstatic,讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)。
    • invokestatic:呼叫一個型別的靜態方法的時候。
  • 使用java.lang.reflect

    包對型別進行反射呼叫的時候。

  • 當初始化類的時候,發現父類還沒有進行初始化,則先初始化父類。

  • 虛擬機器啟動時,會先初始化使用者指定執行的主類,main方法所在類。

  • 當使用JDK 7新加入的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解 析結果為REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四種類型的方法控制代碼,並且這個方法控制代碼對應的類沒有進行過初始化,則需要先觸發其初始化。

  • 當一個介面中定義了JDK 8新加入的預設方法(被default關鍵字修飾的介面方法)時,如果有 這個介面的實現類發生了初始化,那該介面要在其之前被初始化。

類載入的過程

系統載入Class型別的檔案主要是三步:載入,連線,初始化。

連線過程又分為三步:驗證,準備,解析。

載入

在載入階段Java虛擬機器需要完成的三件事情:

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

一個非陣列類的載入階段(載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類載入器去控制位元組流的獲取方式(重寫一個類載入器的 loadClass()

方法)。陣列型別不通過類載入器建立,它由 Java 虛擬機器直接建立。

驗證

確保Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》的全部約束要求,保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全。

  • 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範。
    • 是否以0XCAFEBABE開頭。
    • 主次版本號是否在當前虛擬機器的處理範圍之內。
    • 常量池中的常量是否有不被支援的型別。
  • 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。
    • 這個類是否有父類。【除Object之外都有父類】
    • 這個類是否被繼承了不允許繼承的類。【final修飾的類】
  • 位元組碼驗證:最複雜的階段,通過資料流和控制流分析,確定程式語義是否合法、符合邏輯。
    • 比如說任意時刻運算元棧和指令程式碼序列都能夠配合工作。
  • 符號引用驗證:確保解析動作能正確執行。

準備

為類中定義的變數,分配記憶體設定類變數初始值的階段。

  • 僅僅進行包括類變數的記憶體分配【static】,不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
  • 初始值通常情況下時資料型別的零值(int型別為0,boolean型別為false等),特殊情況如類欄位的欄位中屬性表中存在ConstantValue屬性時:public static final int value = 123,在準備階段就會賦值123。

tips:public static int value = 123;該語句在準備階段過後為value值為0,給value賦值123的操作在類初始化才進行。

解析

Java虛擬機器將常量池內的符號引用替換為直接引用的過程

解析動作主要針對7類符號引用:類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫限定符。

Java 虛擬機器為每個類都準備了一張方法表來存放類中所有的方法。當需要呼叫一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接呼叫該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被呼叫。

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,也就是得到類或者欄位、方法在記憶體中的指標或者偏移量。

初始化

類載入的最後一個過程,前面的幾個階段中,除了載入階段使用者可以自定義載入器參與控制外,其餘動作完全 由JVM主導,直到初始化階段,JVM才真正開始執行類中編寫的Java程式碼,主導權交由應用程式。

初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的 語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問 到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。

JVM會保證子類的<clinit>()方法執行前,父類的<clinit>()方法也已經執行完畢,因此第一個被執行的<clinit>()方法一定在java.lang.Object中。

Java虛擬機器必須保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖同步,如果多個執行緒同 時去初始化一個類,那麼只會有其中一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等 待,直到活動執行緒執行完畢<clinit>()方法。如果在一個類的<clinit>()方法中有耗時很長的操作,那就 可能造成多個程序阻塞。

且其他執行緒喚醒之後就不會再進入該方法,從而保證再同一個類載入器之下,一個型別只會被初始化一次。


對於初始化階段,JDK8之前虛擬機器嚴格規範了有且只有5種情況下,必須對類進行初始化(只有主動去使用類才會初始化類):

  1. 當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態欄位(未被 final 修飾)、或呼叫一個類的靜態方法時。
    • 當jvm執行new指令時會初始化類。即當程式建立一個類的例項物件。
    • 當jvm執行getstatic指令時會初始化類。即程式訪問類的靜態變數(不是靜態常量,常量會被載入到執行時常量池)。
    • 當jvm執行putstatic指令時會初始化類。即程式給類的靜態變數賦值。
    • 當jvm執行invokestatic指令時會初始化類。即程式呼叫類的靜態方法。
  2. 使用 java.lang.reflect 包的方法對類進行反射呼叫時如Class.forname("..."),newInstance()等等。 ,如果類沒初始化,需要觸發其初始化。
  3. 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
  4. 當虛擬機器啟動時,使用者需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機器會先初始化這個類。
  5. MethodHandle和VarHandle可以看作是輕量級的反射呼叫機制,而要想使用這2個呼叫, 就必須先使用findStaticVarHandle來初始化要呼叫的類。

JDK8新增:

  1. 當一個介面中定義了JDK8新加入的預設方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實現類發生了初始化,那該介面要在其之前被初始化。