一夜搞懂 | JVM 類載入機制

許朋友愛玩?發表於2020-04-03

前言

本文已經收錄到我的Github個人部落格,歡迎大佬們光臨寒舍:

我的GIthub部落格

學習導圖

學習導圖

一.為什麼要學習類載入機制?

今天想跟大家嘮嗑嘮嗑Java的類載入機制,這是Java的一個很重要的創新點,曾經也是Java流行的重要原因之一。

Oracle當初引入這個機制是為了滿足Java Applet開發的需求,JVM咬咬牙引入了Java類載入機制,後來的基於Jvm的動態部署,外掛化開發包括大家熱議的熱修復,總之很多後來的技術都源於在JVM中引入了類載入器。

如今,類載入機制也在各個領域大放異彩,在面試中,由類載入機制所衍生出來各類面試題也層出不窮。

所以,我們要了解下類載入機制,為工作中或者是面試中實際的需要打好良好的基礎。

二.核心知識點歸納

2.1 概述

Q1:JVM類載入機制定義

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

Q2:特性

執行期類載入。即在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期完成的,從而通過犧牲一些效能開銷來換取Java程式的高度靈活性

什麼是執行期,什麼是編譯期?

  • 編譯期是指編譯器將原始碼翻譯機器能識別的程式碼Java被編譯為Jvm認識的位元組碼檔案
  • 執行期則是指Java程式碼的執行過程

JVM執行期動態載入+動態連線->Java的動態擴充套件特性

2.2 類載入的過程

類從被載入到虛擬機器記憶體中開始、到解除安裝出記憶體為止,整個生命週期包括七個階段:

  • 載入

  • 驗證

  • 準備

  • 解析

  • 初始化

  • 使用

  • 解除安裝

其中,驗證、準備、解析這3個部分統稱為連線,流程如下圖:

類載入過程

注意:

  • 『載入』->『驗證』->『準備』->『初始化』->『解除安裝』這五個階段的順序是確定的,而『解析』可能為了支援Java的動態繫結會在『初始化』後才開始
  • 上述階段通常都是互相交叉地混合式進行的,比如會在一個階段執行的過程中呼叫、啟用另外一個階段

想要了解Java動態繫結和靜態繫結區別的話,可以看下這篇文章:理解靜態繫結與動態繫結

2.2.1 載入

Q1:任務

  • 通過類的全限定名來獲取定義此類的二進位制位元組流。如從ZIP包讀取、從網路中獲取、通過執行時計算生成、由其他檔案生成、從資料庫中讀取等等途徑......

想要詳細瞭解類的全限定名的知識,可以看下這篇文章:全限定名、簡單名稱和描述符是什麼東西?

  • 將該二進位制位元組流所代表的靜態儲存結構轉化為方法區執行時資料結構,該資料儲存資料結構由虛擬機器實現自行定義
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,它將作為程式訪問方法區中的這些型別資料的外部介面

2.2.2 驗證

  • 連線階段的第一步,且工作量在JVM類載入子系統中佔了相當大的一部分
  • 目的:為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全

由此可見,它能直接決定JVM能否承受惡意程式碼的攻擊,因此驗證階段很重要,但由於它對程式執行期沒有影響,並不一定必要,可以考慮使用-Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。

  • 檢驗過程包括下面四個階段:

    A.檔案格式驗證:

    • 內容:驗證位元組流是否符合Class檔案格式的規範、以及是否能被當前版本的虛擬機器處理

    • 目的:保證輸入的位元組流能正確地解析並儲存於方法區之內,且格式上符合描述一個Java型別資訊的要求。只有保證二進位制位元組流通過了該驗證後,它才會進入記憶體的方法區中進行儲存,所以後續3個驗證階段全部是基於方法區而不是位元組流了

    • 例子:

      1. 是否以魔數0xCAFEBABE開頭

      2. 主次版本號是否在JVM接受範圍內

      3. 索引值是否有指向不存在/不符合型別的常量

        ......

    B.後設資料驗證:

    • 內容:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求

    • 目的:對類的後設資料資訊進行語義校驗,保證不存在不符合Java語言規範的後設資料資訊

    • 例子:

      1. 類是否有父類(除了java.lang.Object之外,所有類都應有父類)

      2. 父類是否繼承了不允許被繼承的類(final修飾的類)

      3. 如果該類不是抽象類,是否實現了其父類或介面中要求實現的所有方法

        ......

    ​ C.位元組碼驗證:

    • 是驗證過程中最複雜的一個階段

    • 內容:對類的方法體進行校驗分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的事件

    • 目的:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的

    • 例子:

      1. 保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,例如不會出現“在運算元棧的資料型別中放置了int型別的資料,使用時卻按long型別來載入本地變數表中”

      2. 保證任何跳轉指令都不會跳轉到方法體外的位元組碼指令上

        ......

    ​ D.符號引用驗證:

    • 內容:對類自身以外(如常量池中的各種符號引用)的資訊進行匹配性校驗
    • 目的:確保解析動作能正常執行,如果無法通過符號引用驗證,那麼將會丟擲一個java.lang.IncompatibleClassChangeError異常的子類
    • 注意:該驗證發生在虛擬機器將符號引用轉化為直接引用的時候,即『解析』階段

2.2.3 準備

Q1:任務

  • 為類變數(靜態變數)分配記憶體因為這裡的變數是由方法區分配記憶體的,所以僅包括類變數而不包括例項變數,後者將會在物件例項化時隨著物件一起分配在Java堆中
  • 設定類變數初始值:通常情況下零值

2.2.4 解析

之前提過,解析階段就是虛擬機器將常量池內的符號引用替換為直接引用的過程

  • 符號引用:以一組符號來描述所引用的目標
  • 可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可
  • 與虛擬機器實現的記憶體佈局無關,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中,所以即使各種虛擬機器實現的記憶體佈局不同,但是能接受符號引用都是一致的
  • 直接引用:
  • 可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼
  • 與虛擬機器實現的記憶體佈局相關,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不同
  • 發生時間:JVM會根據需要來判斷,是在類被載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才去解析
  • 解析動作:有七類符號及其對應在常量池的七種常量型別
  • 類或介面(CONSTANT_Class_info)
  • 欄位(CONSTANT_Fieldref_info)
  • 類方法(CONSTANT_Methodref_info)
  • 介面方法(CONSTANT_InterfaceMethodref_info)
  • 方法型別(CONSTANT_MethodType_info)
  • 方法控制程式碼(CONSTANT_MethodHandle_info)
  • 呼叫點限定符(CONSTANT_InvokeDynamic_info)

舉個例子,設當前程式碼所處的為類D,把一個從未解析過的符號引用N解析為一個類或介面C的直接引用,解析過程分三步:

  • C不是陣列型別:JVM將會把代表N的全限定名傳遞給D類載入器去載入這個類C。在載入過程中,由於後設資料驗證位元組碼驗證的需要,又可能觸發其他相關類的載入動作。一旦這個載入過程出現了任何異常,解析過程就宣告失敗。
  • C是陣列型別且陣列元素型別為物件:JVM也會按照上述規則載入陣列元素型別
  • 若上述步驟無任何異常:此時CJVM中已成為一個有效的類或介面,但在解析完成前還需進行符號引用驗證,來確認D是否具備對C的訪問許可權。如果發現不具備訪問許可權,將丟擲java.lang.IllegalAccessError異常

Q1:欄位(成員變數/域)和屬性有什麼區別?

  • 屬性,是指物件的屬性,對於JavaBean來說,是getXXX方法定義的
  • 欄位,是成員變數
class Person{
    private String mingzi;  //mingzi是欄位,一般來說欄位和屬性是相同的,但是這個例子是特例
    public String getName(){  //name是屬性
        return mingzi:
    }
    public void setName(){
        mingzi= "張三";
    }
}
複製程式碼

2.2.5 初始化

  • 是類載入過程的最後一步,會開始真正執行類中定義的Java程式碼。而之前的類載入過程中,除了在『載入』階段使用者應用程式可通過自定義類載入器參與之外,其餘階段均由虛擬機器主導和控制
  • 與『準備』階段的區分
  • 準備階段:變數賦初始零值
  • 初始化階段:根據Java程式的設定去初始化類變數和其他資源,或者說是執行類構造器clinit的過程

clinit:由編譯器自動收集類中的所有類變數(靜態變數)的賦值動作和靜態語句塊static{}中的語句合併產生

  • 執行緒安全的,在多執行緒環境中被正確地加鎖、同步
  • 對於類或介面來說是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成 clinit
  • 介面與類不同的是,執行介面的 clinit不需要先執行父介面clinit,只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的clinit

想詳細瞭解clinit以及其與init的區別的讀者,可以看下這篇文章:深入理解jvm--Java中init和clinit區別完全解析

  • 在虛擬機器規範中,規定了有且只有五種情況必須立即對類進行『初始化』:
  • 遇到newgetstaticputstaticinvokestatic這4條位元組碼指令時
  • 使用java.lang.reflect包的方法對類進行反射呼叫的時候
  • 當初始化一個類的時候,若發現其父類還未進行初始化,需先觸發其父類的初始化
  • 在虛擬機器啟動時,需指定一個要執行的主類,虛擬機器會先初始化它
  • 當使用JDK1.7的動態語言支援時,若一個java.lang.invoke.MethodHandle例項最後的解析結果為REF_getStaticREF_putStaticREF_invokeStatic的方法控制程式碼,且這個方法控制程式碼所對應的類未進行初始化,需先觸發其初始化。

2.3 類載入器&雙親委派模型

每個類載入器,都擁有一個獨立的名稱空間,它不僅用於載入類,還和這個類本身一起作為在JVM中的唯一標識。所以比較兩個類是否相等,只要看它們是否由同一個類載入器載入,即使它們來源於同一個Class檔案且被同一個JVM載入,只要載入它們的類載入器不同,這兩個類就必定不相等

2.3.1 類載入器

JVM的角度,可將類載入器分為兩種:

  • 啟動類載入器
  • C++語言實現,是虛擬機器自身的一部分
  • 負責載入存放在<JAVA_HOME>\lib目錄中、或被-Xbootclasspath引數所指定路徑中的、且可被虛擬機器識別的類庫
  • 無法被Java程式直接引用,如果自定義類載入器想要把載入請求委派給引導類載入器的話,可直接用null代替
  • 其他類載入器:由Java語言實現,獨立於虛擬機器外部,並且全都繼承自抽象類java.lang.ClassLoader,可被Java程式直接引用。常見幾種:
  • 擴充套件類載入器

    A.由sun.misc.Launcher$ExtClassLoader實現

    B.負責載入<JAVA_HOME>\lib\ext目錄中的、或者被java.ext.dirs系統變數所指定的路徑中的所有類庫

  • 應用程式類載入器

    A.是預設的類載入器,是ClassLoader#getSystemClassLoader()的返回值,故又稱為系統類載入器

    B.由sun.misc.Launcher$App-ClassLoader實現

    C.負責載入使用者類路徑上所指定的類庫

  • 自定義類載入器:如果以上類載入起不能滿足需求,可自定義

類載入器的關係

需要注意的是:雖然陣列類不通過類載入器建立而是由JVM直接建立的,但仍與類載入器有密切關係,因為陣列類的元素型別最終還要靠類載入器去建立

2.3.2 雙親委派模型

  • 定義:表示類載入器之間的層次關係

  • 前提:除了頂層啟動類載入器外,其餘類載入器都應當有自己的父類載入器,且它們之間關係一般不會以繼承關係來實現,而是通過組合關係來複用父載入器的程式碼

  • 工作過程:若一個類載入器收到了類載入的請求,它先會把這個請求委派給父類載入器,並向上傳遞,最終請求都傳送到頂層的啟動類載入器中。只有當父載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去載入

  • 注意:不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入器實現方式

  • 優點

    1. 類會隨著它的類載入器一起具備帶有優先順序的層次關係,可保證Java程式的穩定運作

    2. 實現簡單,所有實現程式碼都集中在java.lang.ClassLoader的loadClass()

  • 比如,某些類載入器要載入java.lang.Object類,最終都會委派給最頂端的啟動類載入器去載入,這樣Object類在程式的各種類載入器環境中都是同一個類

  • 相反,系統中將會出現多個不同的Object類,Java型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂

三.課堂小測試

恭喜你!已經看完了前面的文章,相信你對JVM類載入機制已經有一定深度的瞭解,下面,進行一下課堂小測試,驗證一下自己的學習成果吧!

Q1:類載入的全過程是怎樣的?

Q2:什麼是雙親委派模型?

Q3:String類如何被載入的

上面問題的答案,在前文都提到過,如果還不能回答出來的話,建議回顧下前文

Q4:請你談談類載入過程,以Person a = new Person();為例進行說明

這道題是在牛客的暑假實習Tencent一面的麵筋上找的,附上標準答案:類的載入過程,Person person = new Person();為例進行說明


如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力

本文參考連結:

相關文章