深入理解Java虛擬機之——類加載機制

聲明:原創作品,轉載請註明出處https://www.jianshu.com/p/336a6f7dd413

Java是一門面向對象的語言,萬物皆對象,萬物都可以用一個類來描述。當我們想要描述一個事物的時候,我們會先創建一個.class文件,然後使用的時候只需要在代碼中new下,這樣這個類的實例對象就出來了。接着就可以調用這個對象的各種之前已經定義的方法。那麼就有個問題,就是一個.class文件是如何進入到我們的虛擬機中的,進入之後又做了怎樣的處理。其實這一系列過程我們稱之爲類加載機制

一.類的生命週期

一個類在虛擬機中完整的生命週期包括以下幾個階段:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)。其中驗證、準備、解析三個部分統稱爲連接(Linking)。順序如下圖:



加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,但是解析階段不一樣,有可能會在初始化之後,這是爲了支持Java語言的運行時綁定。
類的加載過程其實只是上面的加載到初始化過程,接下來我們來挨個看下這幾個階段:

二.類的加載過程

1.加載

這一階段虛擬機主要要做的三件事情是:

  • 1.通過此類的全限定名來獲取此類的二進制字節流
    1. 將這個二進制靜態存儲結構轉換爲方法區的運行時數據結構
  • 3.在Java堆中生成一個代表這個類的Class對象,作爲方法區這些數據的訪問入口

2.驗證

驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證分四個階段:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
a.文件格式驗證
第一階段要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。
b.元數據驗證
第二階段是對字節碼描述的信息進行予以分析,以保證其描述的信息符合Java語言規範的要求。
c.字節碼驗證
第三階段是整個驗證過程中最複雜的一個階段,主要工作是進行數據流和控制流分析。在第二階段對元數據信息中的數據類型做完校驗後,這個階段對類的方法體進行校驗分析。這個階段的任務是保證被校驗類的方法在運行時不會做出危害虛擬機安全的行爲。
d.符號引用驗證
最後一個階段的校驗發生在虛擬機將符號引用轉爲直接引用的時候,這個轉換動作將在連接的第三個階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,其目的是確保解析動作能正常執行。

3.準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。這裏的類變量指的是被static修飾的變量,這裏設置的初始值爲0值。如下

public static int value  = 123;

那麼變量value在準備階段過後其值爲0而不是123,當然如果這個value是被final修飾,那麼在準備階段會直接設置爲123,如下:

public static final int value = 123;

4.解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標不一定已經加載到內存中。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

對同一個符號引用進行多次解析請求是很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存。解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用進行。

a.類或接口的解析
假設當前代碼所處的類爲D,如果要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,那虛擬機完成整個解析的過程需要包括以下3個步驟:

  • 1.如果C不是一個數組類型,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類。在加載過程中,由於無數據驗證、字節碼驗證的需要,又將可能觸發其他相關類的加載動作,例如加載這個類的父類或實現的接口。一旦這個加載過程出現任何異常,解析過程就將宣告失敗。
  • 2.如果C是一個數組類型,並且數組的元素類型爲對象,也就是N的描述符會是類似[java.lang.Integer的形式,那將會按照第一點的規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載的元素類型就是java.lang.Integer,接着由虛擬機生成一個代表此數組維度和元素的數組對象。
  • 3.如果上面的步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證,確認C是否具備對D的訪問權限。如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError異常。

b.字段解析
在解析字段之前先要解析字段所屬的類或接口,如果解析成功,那將這字段所屬的類或接口用C表示,虛擬機規範要求按照如下步驟對C進行後續字段的解析:

  • 1.如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  • 2.否則,如果在C中實現了接口,將會按照繼承關係從上往下遞歸搜索各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標想匹配的字段,則返回這個字段的直接引用,查找結束。
  • 3.否則,如果C不是java.lang.Object的話,將會按照繼承關係從上往下遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  • 4.否則,查找失敗,拋出java.lang.NoSuchFieldError異常。

如果查找過程成功返回了引用,將會對這個字段進行權限驗證,如果發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。

c.類方法解析
類方法解析的第一個步驟與字段解析一樣,需要先解析方法所屬的類或接口,如果成功我們依然用C表示這個類,接下來虛擬機將會按照如下步驟進行後續的類方法解析:

  • 1.類方法和接口方法符號引用的常量類型定義是分開的,如果C是一個接口,則直接拋出java.lang.IncompatibleClassChangeError異常。
  • 2.如果通過了上面這步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 3.否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 4 否則,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在相匹配的方法,說明類C是一個抽象類,這個時候查找結束,拋出java.lang.AbstractMethodError異常。
  • 5.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

最後如果查找過程成功返回了直接引用,將會對這個方法進行權限驗證;如果發現不具備對此方法的訪問權限,將拋出java.lang.IllegalAccessError異常。

d.接口方法解析
同上,解析接口方法時需先解析方法所屬的類或符號的符號引用,如果成功,依然用C表示這個接口,解析步驟如下:

  • 1.與類方法解析相反,如果發現這個C是類不是接口,則直接拋出java.lang.IncompatibleClassChangeError異常。
  • 2.否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 3.否則,在接口C的父接口中遞歸查找,直到java.lang.Object類爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 4.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

由於接口中的所有方法都默認是public,所以不存在訪問權限的問題,因此接口方法的符號解析應當不會拋出java.lang.IllegalAccessError異常。

5.初始化

類初始化階段是類加載過程的最後一步,之前的階段幾乎都是由虛擬機主導和控制,這一階段才真正執行自己定義的Java程序代碼。初始化階段是執行類構造器<clinit>()方法的過程。來看下<clinit>()方法相關說明:

  • <clinit>()方法是由編譯器自動蒐集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器蒐集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。
  • <clinit>()方法與類的構造函數不同,它不需要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。
  • 由於父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作
  • <clinit>()方法對於類或接口來說並不是必須的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成<clinit>()方法。
  • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法,但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父類接口中定義的變量被使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。
  • 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

3.類加載器

類加載器的功能很簡單,就是通過一個類的全限定名來獲取描述此類的二進制字節流。類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。

類加載器的分類

類加載分三種:啓動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用程序類加載器(Application ClassLoader)。其中啓動類加載器是由C++語言實現,是虛擬機自身的一部分,另外兩種均有Java語言實現,獨立於虛擬機外部並且全部繼承自抽象類java.lang.ClassLoader。

  • 啓動類加載器:負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用。
  • 擴展類加載器:這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器:這個類加載器由sun.msc.Launcher$AppClassLoader來實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委託模型

我們的應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關係如下:


上面的這種層次關係,就稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父類關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父加載器的代碼。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載器的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試自己去加載。
雙親委派模型實現原理:其代碼實現都在java.lang.ClassLoader的loadClass方法中,首先會檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,則在拋出ClassNotFoundException異常後,再調用自己的findClass方法進行加載。

破壞雙親委派模型

雙親委派模型主要出現過三次被破壞情況
第一次破壞
雙親委派模型是JDK1.2之後才被引入的,需要自定義類加載器時,之前都是直接重寫ClassLoader的loadClass()方法,而引入雙親委派模型後只需要在findClass中實現,所以之前的方式不符合雙親委派模型。
第二次破壞
上面我們提到啓動類加載器都是加載一些API中基礎的類,但是有的時候需要用啓動類加載器加載自己的代碼,這就打破原有的雙親委派模型,比如JNDI服務。
第三次破壞
關於OSGi等熱部署技術,OSGi是一種平級的加載,是一種網狀結構,而不是上述的雙親委派結構,是一種樹狀結構。

參考書籍:《深入理解Java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章