1. 程式人生 > 實用技巧 >JVM類載入機制過程詳解

JVM類載入機制過程詳解

JVM類載入機制

Java程式執行過程

  • 首先通過Javac編譯器將==.java轉為JVM可載入的.class==位元組碼檔案。

    javac是由Java編寫的程式,編譯過程可分為

    • 詞法分析。通過空格分割出單詞、操作符、控制符等資訊,形成token資訊流,傳遞給語法解析器。
    • 語法解析。把token資訊流按照Java語法規則組裝成語法樹。
    • 語義分析。檢查關鍵字使用是否合理、型別是否匹配、作用域是否相等。
    • 位元組碼生成。將前面各個步驟的資訊轉換成位元組碼。

    位元組碼必須通過類載入過程載入到JVM才可以執行,執行有三種模式,解釋執行、JIT編譯執行、JIT編譯與直譯器混合執行(主流JVM預設執行的方式)。混合模式的優勢在於直譯器在啟動時先解釋執行,省去編譯時間。

  • 之後通過即時編譯器JIT把位元組碼檔案編譯成本地機器碼。

    Java程式最初都是通過直譯器進行解釋執行的,當虛擬機發現某個方法或程式碼塊的執行特別頻繁,就會認定其為“熱點程式碼”,熱點程式碼的檢測主要有基於取樣和基於計數器兩種方式,為了提高熱點程式碼的執行效率,虛擬機器會把它們編譯成本地機器碼,儘可能對程式碼優化,在執行時完成這個任務的後端編譯器被稱為即時編譯器。

  • 還可以通過靜態的提前編譯器AOT直接把程式編譯成與目標機器指令集相關的二進位制程式碼。

    在這裡插入圖片描述

類載入

Class檔案中描述的各類資訊都需要載入到虛擬機器後才能使用。JVM把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這個過程稱為虛擬機器的類載入機制。

與編譯時需要連線的語言不同,Java中型別的載入、連線和初始化都是在執行期間完成的,這增加了效能開銷,但卻提供了極高的擴充套件性,Java動態擴充套件的語言特性就是依賴執行期動態載入和連線實現的。

一個型別從被載入到虛擬機器開始,到卸載出記憶體為止,整個生命週期經歷載入、驗證、準備、解析、初始化、使用和解除安裝七個階段,其中驗證、解析和初始化三個部分稱為連線。載入、驗證、準備、初始化階段的順序時確定的,解析則不一定:可能在初始化後再開始,這是為了支援Java的動態繫結。


類載入的過程

在這裡插入圖片描述

載入

該階段虛擬機器需要完成三件事:

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

注意:這裡不一定非要從一個Class檔案獲取,這裡既可以從ZIP包中讀取(比如從jar包和war包中讀取),也可以在執行時計算生成(動態代理),也可以由其它檔案生成(比如將JSP檔案轉換成對應的Class類)。

驗證

這一階段是確保Class檔案的位元組流符合約束。如果虛擬機器不檢查輸入的位元組流,可能因為載入有錯誤或惡意企圖的位元組流而導致系統受攻擊。驗證主要包含四個階段:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

驗證重要但非必需,因為只有通過與否的區別,通過後對程式執行期沒有任何影響。如果程式碼已被反覆使用和驗證過,在生產環境就可以考慮關閉大部分驗證縮短類載入時間。

準備

為類靜態變數分配記憶體並設定零值,該階段進行的記憶體分配僅包括類變數,不包括例項變數。如果變數被final修飾,編譯時Javac會為變數生成ConstantValue屬性,準備階段虛擬機器會將變數值設為程式碼值。

比如:

public static int v = 8080;

實際上變數V在準備階段過後的初始值為0,而不是8080,將v賦值為8080的put static指令是程式編譯後,存放於類構造器中

但如果宣告為:

public static final int v = 8080;

在編譯階段會為生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將v賦值為8080.

解析

該階段虛擬機器將常量池中的符號引用替換為直接引用的過程。

符號引用:以一組符號描述為引用目標,可以是任何形式的字面量,只要使用時能無歧義地定位目標即可。與虛擬機器記憶體佈局無關,引用目標不一定已經載入到虛擬機器記憶體。

直接引用:是可以直接指向目標的指標、相對偏移量或能間接定位到目標的控制代碼。和虛擬機器的記憶體佈局無關,引用目標必須已在虛擬機器的記憶體中存在。

初始化

直到該階段JVM才開始執行類中編寫的程式碼。準備階段時變數賦過零值,初始化階段會根據程式設計師的編碼去初始化類變數和其他資源。初始化階段就是執行類構造方法中的方法,該方法是javac自動生成的。

使用

解除安裝

類構造器

初始化階段就是執行類構造方法中的方法的過程。方法是由編譯器自動收集類中的類變數的賦值操作和靜態語句塊中的語句合併而成的。虛擬機器會保證子方法執行之前,父類的方法已經執行完畢,如果一個類中沒有對靜態變數賦值也沒有靜態語句塊,那麼編譯器可以不為這個類生成()方法。

  • 遇到newgetstaticputstaticinvokestatic位元組碼指令時,還未初始化。典型場景包括new例項化物件、讀取或設定靜態欄位、呼叫靜態方法。

  • 對類反射呼叫時,還未初始化。

  • 初始化類時,父類還沒有初始化。

  • 虛擬機器啟動時,會先初始化包含main方法的主類。

  • 使用JDK7的動態語言支援時,如果MethodHandle例項的解析結果為指定型別的方法控制代碼對應的類還未初始化。

  • 介面定義了預設方法,如果介面的實現類初始化,介面要在之前初始化。

    其餘所有引用型別的方式都不會觸發初始化,稱為被動引用。被動引用例項:

    • 子類使用父類靜態欄位時,只有父類被初始化
    • 通過陣列定義使用類
    • 常量在編譯期會存入呼叫類的常量池,不會初始化定義常量的類。

介面和類載入過程的區別:

初始化類時如果父類沒有初始化需要初始化父類,但介面初始化時不要求父介面初始化,只有在真正使用父介面時(如引用介面中定義的常量)才會初始化。

類載入器

在這裡插入圖片描述

啟動類載入器

在JVM啟動時建立,負責載入最核心的類,例如Object、System等。無法被程式直接引用,如果需要把載入委派給啟動類載入,直接使用null代替即可,因為啟動類載入器通常由作業系統實現,並不存在於JVM體系。

擴充套件類載入器

從JDK9開始從擴充套件類載入器更換為平臺載入器,負載載入一些擴充套件的系統類,比如XML、加密、壓縮相關的功能類等。

應用程式類載入器

也稱系統類載入器。負責載入使用者類路徑(classpath)上的類庫,可以直接在程式碼中使用,如果沒有自定義類載入器,一般情況下應用類載入器就是預設的類載入器。自定義類載入器通過繼承Class Loader並重寫findClass方法實現。

雙親委派模型

類載入器具有等級制度但並非繼承關係,以組合的方式複用父載入器的功能。雙親委派模型要求除了頂層的啟動類載入器外,其餘類載入器都應該有自己的父載入器。

一個類載入器收到了類載入請求,它不會自己去嘗試載入,而是將該請求委派給父載入器,每層的類載入器都是如此,因此所有載入請求最終都應該傳送到啟動類載入器,只有當父載入器反饋無法完成請求時,子載入器才會嘗試。

類跟隨它的載入器一起具備了有優先順序的層次關係,確保某個類在各個類載入器環境中都是同一個,保證程式的穩定性。

在這裡插入圖片描述

雙親委派機制的作用

  1. 防止重複載入同一個.class。通過委託去向上面問一問,載入過了,就不用再載入一遍。保證資料安全。
    類載入器環境中都是同一個,保證程式的穩定性。

[外鏈圖片轉存中…(img-I4TJP7vw-1602771672834)]

雙親委派機制的作用

  1. 防止重複載入同一個.class。通過委託去向上面問一問,載入過了,就不用再載入一遍。保證資料安全。
  2. 保證核心.class不能被篡改。通過委託方式,不會去篡改核心.class,即使篡改也不會去載入,即使載入也不會是同一個.class物件了。不同的載入器載入同一個.class也不是同一個Class物件。這樣保證了Class執行安全。