java虛擬機器類載入機制

一寸HUI發表於2020-05-25

一、概述

1.1、概念

  Java虛擬機器把描述類的資料從Class檔案載入到記憶體, 並對資料進行校驗、 轉換解析和初始化,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構,最終形成可以被虛擬機器直接使用的Java型別, 這個過程被稱作虛擬機器的類載入機制。

  類載入器並不需要等到某個類被“首次主動使用”時再載入它,JVM規範允許類載入器在預料某個類將要被使用時就預先載入它。

1.2、載入class檔案的途徑

  • 從本地系統中直接載入

  • 通過網路下載.class檔案

  • 從zip,jar等歸檔檔案中載入.class檔案

  • 從專有資料庫中提取.class檔案

  • 將Java原始檔動態編譯為.class檔案

二、類載入時機

  一個型別從被載入到虛擬機器記憶體中開始, 到解除安裝出記憶體為止, 它的整個生命週期將會經歷載入(Loading) 、 驗證(Verification) 、 準備(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和解除安裝(Unloading) 七個階段, 其中驗證、 準備、 解析三個部分統稱為連線(Linking)。這七個階段(類的生命週期)如下圖所示:

 

  載入、 驗證、 準備、 初始化和解除安裝這五個階段的順序是確定的, 型別的載入過程必須按照這種順序按部就班地開始, 而解析階段則不一定: 它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結特性(也稱為動態繫結或晚期繫結)

  關於在什麼情況下需要開始類載入過程的第一個階段“載入”,並沒有進行強制約束, 這點可以交給虛擬機器的具體實現來自由把握。 但是對於初始化階段,有且只有六種情況必須立即對類進行“初始化”(而載入、 驗證、 準備自然需要在此之前開始) :

1、遇到new、 getstatic、 putstatic或invokestatic這四條位元組碼指令時, 如果型別沒有進行過初始化, 則需要先觸發其初始化階段。 能夠生成這四條指令的典型Java程式碼場景有:

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

2、使用java.lang.reflect包的方法對型別進行反射呼叫的時候, 如果型別沒有進行過初始化, 則需要先觸發其初始化。

3、當初始化類的時候, 如果發現其父類還沒有進行過初始化, 則需要先觸發其父類的初始化。

4、當虛擬機器啟動時, 使用者需要指定一個要執行的主類(包含main()方法的那個類) , 虛擬機器會先初始化這個主類

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

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

三、類載入過程

3.1、載入

  載入”(Loading) 階段是整個“類載入”(Class Loading) 過程中的一個階段, 希望讀者沒有混淆,這兩個看起來很相似的名詞。 在載入階段, Java虛擬機器需要完成以下三件事情:

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

  非陣列型別的載入階段(準確地說, 是載入階段中獲取類的二進位制位元組流的動作) 是開發人員可控性最強的階段。 載入階段既可以使用Java虛擬機器裡內建的引導類載入器來完成, 也可以由使用者自定義的類載入器去完成, 開發人員通過定義自己的類載入器去控制位元組流的獲取方式(重寫一個類載入器的findClass()或loadClass()方法) , 實現根據自己的想法來賦予應用程式獲取執行程式碼的動態性

  對於陣列類而言, 情況就有所不同, 陣列類本身不通過類載入器建立, 它是由Java虛擬機器直接在記憶體中動態構造出來的。 但陣列類與類載入器仍然有很密切的關係, 因為陣列類的元素型別(ElementType, 指的是陣列去掉所有維度的型別) 最終還是要靠類載入器來完成載入

  載入階段結束後, Java虛擬機器外部的二進位制位元組流就按照虛擬機器所設定的格式儲存在方法區之中了, 方法區中的資料儲存格式完全由虛擬機器實現自行定義, 會在Java堆記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區中的型別資料的外部介面。

  載入階段與連線階段的部分動作(如一部分位元組碼檔案格式驗證動作) 是交叉進行的, 載入階段尚未完成, 連線階段可能已經開始, 但這些夾在載入階段之中進行的動作, 仍然屬於連線階段的一部分, 這兩個階段的開始時間仍然保持著固定的先後順序

3.2、驗證

  驗證的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》 的全部約束要求, 保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全,驗證階段大致上會完成下面四個階段的檢驗動作: 檔案格式驗證、 後設資料驗證、 位元組碼驗證和符號引用驗證

3.2.1、檔案格式驗證

第一階段要驗證位元組流是否符合Class檔案格式的規範, 並且能被當前版本的虛擬機器處理。 這一階段可能包括下面這些驗證點(舉例說明):

  • 是否以魔數0xCAFEBABE開頭。
  • 主、 次版本號是否在當前Java虛擬機器接受範圍之內。
  • 常量池的常量中是否有不被支援的常量型別(檢查常量tag標誌) 。
  • 指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的資料。
  • Class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊

  該驗證階段的主要目的是保證輸入的位元組流能正確地解析並儲存於方法區之內, 格式上符合描述一個Java型別資訊的要求(驗證位元組流是否符合Class檔案格式的規範)。 這階段的驗證是基於二進位制位元組流進行的, 只有通過了這個階段的驗證之後, 這段位元組流才被允許進入Java虛擬機器記憶體的方法區中進行儲存, 所以後面的三個驗證階段全部是基於方法區的儲存結構上進行的, 不會再直接讀取、 操作位元組流了

3.2.2、後設資料驗證

後設資料驗證是對位元組碼描述的資訊進行語義分析, 以保證其描述的資訊符合《Java語言規範》 的要求,驗證舉例如下:

  • 這個類是否有父類(除了java.lang.Object之外, 所有的類都應當有父類) 。
  • 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類) 。
  • 如果這個類不是抽象類, 是否實現了其父類或介面之中要求實現的所有方法。
  • 類中的欄位、 方法是否與父類產生矛盾(例如覆蓋了父類的final欄位, 或者出現不符合規則的方法過載, 例如方法引數都一致, 但返回值型別卻不同等)

3.2.3、位元組碼驗證

  位元組碼驗證主要目的是通過資料流分析和控制流分析, 確定程式語義是合法的、 符合邏輯的。 在第二階段對後設資料資訊中的資料型別校驗完畢以後, 這階段就要對類的方法體(Class檔案中的Code屬性) 進行校驗分析, 保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為,驗證舉例如下:

  • 保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作, 例如不會出現類似於“在操作棧放置了一個int型別的資料, 使用時卻按long型別來載入入本地變數表中”這樣的情況。
  • 保證任何跳轉指令都不會跳轉到方法體以外的位元組碼指令上。
  • 保證方法體中的型別轉換總是有效的, 例如可以把一個子類物件賦值給父類資料型別, 這是安全的, 但是把父類物件賦值給子類資料型別, 甚至把物件賦值給與它毫無繼承關係、 完全不相干的一個資料型別, 則是危險和不合法的

3.2.4、符號引用驗證

  符號引用驗證的主要目的是確保解析行為能正常執行,符號引用驗證行為發生在虛擬機器將符號引用轉化為直接引用的時候, 這個轉化動作將在連線的第三階段——解析階段中發生。 符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用) 的各類資訊進行匹配性校驗, 通俗來說就是, 該類是否缺少或者被禁止訪問它依賴的某些外部類、 方法、 欄位等資源。驗證舉例如下:

  • 符號引用中通過字串描述的全限定名是否能找到對應的類。
  • 在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位。
  • 符號引用中的類、 欄位、 方法的可訪問性(private、 protected、 public、 <package>) 是否可被當前類訪問。

  驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用--Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間

3.3、準備

  準備階段是正式為類中定義的變數(即靜態變數, 被static修飾的變數) 分配記憶體並設定類變數初始值的階段, 從概念上講, 這些變數所使用的記憶體都應當在方法區中進行分配, 但必須注意到方法區本身是一個邏輯上的區域, 在JDK 7及之前, HotSpot使用永久代來實現方法區時, 實現是完全符合這種邏輯概念的; 而在JDK 8及之後, 類變數則會隨著Class物件一起存放在Java堆中, 這時候“類變數在方法區”就完全是一種對邏輯概念的表述了

注意點:

  • 首先是這時候進行記憶體分配的僅包括類變數, 而不包括例項變數, 例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
  • 其次是這裡所說的初始值“通常情況”下是資料型別的零值
  • 對基本資料型別來說,對於類變數(static)和全域性變數,如果不顯式地對其賦值而直接使用,則系統會為其賦予預設的零值,而對於區域性變數來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
  • 對於同時被static和final修飾的常量,必須在宣告的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在宣告時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予預設零值。
  • 對於引用資料型別reference來說,如陣列引用、物件引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予預設的零值,即null。
  • 如果在陣列初始化時沒有對陣列中的各元素賦值,那麼其中的元素將根據對應的資料型別而被賦予預設的零值

 

public static int value = 123;

  如上變數value在準備階段過後的初始值為0而不是123, 因為這時尚未開始執行任何Java方法, 而把value賦值為123的putstatic指令是程式被編譯後, 存放於類構造器<clinit>()方法之中, 所以把value賦值為123的動作要到類的初始化階段才會被執行。基本資料型別的初始化的零值如下表所示:

  但是如果類欄位的欄位屬性表中存在ConstantValue屬性, 那在準備階段變數值就會被初始化為ConstantValue屬性所指定的初始值,如下程式碼所示:

public static final int value = 123;

  編譯時Javac將會為value生成ConstantValue屬性, 在準備階段虛擬機器就會根據Con-stantValue的設定將value賦值為123

3.4、解析

  解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或介面、 欄位、 類方法、 介面方法、 方法型別、 方法控制程式碼和呼叫點限定符這7類符號引用進行, 分別對應於常量池的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info、 CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info、 CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8種常量型別。

  符號引用(Symbolic References) : 符號引用以一組符號來描述所引用的目標, 符號可以是任何形式的字面量, 只要使用時能無歧義地定位到目標即可。 符號引用與虛擬機器實現的記憶體佈局無關, 引用的目標並不一定是已經載入到虛擬機器記憶體當中的內容。 各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須都是一致的, 因為符號引用的字面量形式明確定義在Class檔案格式中。

  直接引用(Direct References) : 直接引用是可以直接指向目標的指標、 相對偏移量或者是一個能間接定位到目標的控制程式碼。 直接引用是和虛擬機器實現的記憶體佈局直接相關的, 同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。 如果有了直接引用, 那引用的目標必定已經在虛擬機器的記憶體中存在。

3.5、初始化

  Java虛擬機器才真正開始執行類中編寫的Java程式程式碼, 將主導權移交給應用程式。進行準備階段時, 變數已經賦過一次系統要求的初始零值, 而在初始化階段, 則會根據程式設計師通過程式編碼制定的主觀計劃去初始化類變數和其他資源。 我們也可以從另外一種更直接的形式來表達: 初始化階段就是執行類構造器<clinit>()方法的過程

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

  <clinit>()方法與類的建構函式(即在虛擬機器視角中的例項構造器<init>()方法) 不同, 它不需要顯式地呼叫父類構造器, Java虛擬機器會保證在子類的<clinit>()方法執行前, 父類的<clinit>()方法已經執行完畢。 因此在Java虛擬機器中第一個被執行的<clinit>()方法的型別肯定是java.lang.Object。

  <clinit>()方法對於類或介面來說並不是必需的, 如果一個類中沒有靜態語句塊, 也沒有對變數的賦值操作, 那麼編譯器可以不為這個類生成<clinit>()方法。執行介面的<clinit>()方法,不需要先執行父介面的<clinit>()方法,因為只有當父介面中定義的變數被使用時, 父介面才會被初始化。 此外, 介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法

JVM初始化步驟:
1、假如這個類還沒有被載入和連線,則程式先載入並連線該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:

  • 建立類的例項,也就是new的方式
  • 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  • 呼叫類的靜態方法
  • 反射(如Class.forName(“com.shengsiyuan.Test”))
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類

3.6、結束生命週期

在如下幾種情況下,Java虛擬機器將結束生命週期

  • 執行了System.exit()方法
  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致Java虛擬機器程式終止

四、類載入器

4.1、概述

  對於任意一個類, 都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器中的唯一性, 每一個類載入器, 都擁有一個獨立的類名稱空間。 這句話可以表達得更通俗一些: 比較兩個類是否“相等”, 只有在這兩個類是由同一個類載入器載入的前提下才有意義, 否則, 即使這兩個類來源於同一個Class檔案, 被同一個Java虛擬機器載入, 只要載入它們的類載入器不同, 那這兩個類就必定不相等。

首先來看個小例子:

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

執行結果如下圖所示:

 

 

 結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引導類載入器)是用C語言實現的,找不到一個確定的返回父Loader的方式,於是就返回null。

4.2、類載入器

4.2.1、類載入器的分類

  站在Java虛擬機器的角度來看, 只存在兩種不同的類載入器: 一種是啟動類載入器(BootstrapClassLoader) , 這個類載入器使用C++語言實現[1], 是虛擬機器自身的一部分; 另外一種就是其他所有的類載入器, 這些類載入器都由Java語言實現, 獨立存在於虛擬機器外部, 並且全都繼承自抽象類java.lang.ClassLoader

  站在Java開發人員的角度來看, 類載入器就應當劃分得更細緻一些。 Java一直保持著三層類載入器、 雙親委派的類載入架構, 儘管這套架構在Java模組化系統出現後有了一些調整變動, 但依然未改變其主體結構

  絕大多數Java程式都會使用到以下3個系統提供的類載入器來進行載入,如下圖所示:

 

 

 

  啟動類載入器(Bootstrap Class Loader) :  這個類載入器負責載入存放在<JAVA_HOME>\lib目錄, 或者被-Xbootclasspath引數所指定的路徑中存放的, 而且是Java虛擬機器能夠識別的(按照檔名識別, 如rt.jar、 tools.jar, 名字不符合的類庫即使放在lib目錄中也不會被載入) 類庫載入到虛擬機器的記憶體中。 啟動類載入器無法被Java程式直接引用, 使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器去處理, 那直接使用null代替即可。

  擴充套件類載入器(Extension Class Loader) : 這個類載入器是在類sun.misc.Launcher$ExtClassLoader中以Java程式碼的形式實現的。 它負責載入<JAVA_HOME>\lib\ext目錄中, 或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。 根據“擴充套件類載入器”這個名稱, 就可以推斷出這是一種Java系統類庫的擴充套件機制, JDK的開發團隊允許使用者將具有通用性的類庫放置在ext目錄裡以擴充套件Java SE的功能, 在JDK9之後, 這種擴充套件機制被模組化帶來的天然的擴充套件能力所取代。 由於擴充套件類載入器是由Java程式碼實現的, 開發者可以直接在程式中使用擴充套件類載入器來載入Class檔案

  應用程式類載入器(Application Class Loader) : 這個類載入器由sun.misc.Launcher$AppClassLoader來實現。 由於應用程式類載入器是ClassLoader類中的getSystemClassLoader()方法的返回值, 所以有些場合中也稱它為“系統類載入器”。 它負責載入使用者類路徑(ClassPath) 上所有的類庫, 開發者同樣可以直接在程式碼中使用這個類載入器。 如果應用程式中沒有自定義過自己的類載入器, 一般情況下這個就是程式中預設的類載入器。

4.2.2、類的載入方式

 載入方式如下:

  • 命令列啟動應用時候由JVM初始化載入
  • 通過Class.forName()方法動態載入
  • 通過ClassLoader.loadClass()方法動態載入

例項如下:

public class loaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = Test2.class.getClassLoader();
        System.out.println(loader);  //sun.misc.Launcher$AppClassLoader@18b4aac2
        //使用ClassLoader.loadClass()來載入類,不會執行初始化塊
        loader.loadClass("Test2"); //無輸出
        //使用Class.forName()來載入類,預設會執行初始化塊
         Class.forName("Test2"); // 靜態初始化塊執行了!
        //使用Class.forName()來載入類,並指定ClassLoader,初始化時不執行靜態塊
        Class.forName("Test2", false, loader); //無輸出
    }
}

public class Test2 {
    static {
        System.out.println("靜態初始化塊執行了!");
    }
}

Class.forName()和ClassLoader.loadClass()區別

  • Class.forName():將類的.class檔案載入到jvm中之外,還會對類進行解釋,執行類中的static塊;
  • ClassLoader.loadClass():只幹一件事情,就是將.class檔案載入到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。

注:Class.forName(name, initialize, loader)帶參函式也可控制是否載入static塊。並且只有呼叫了newInstance()方法採用呼叫建構函式,建立類的物件

4.2.3、JVM類的載入機制

  • 全盤負責,當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入
  • 父類委託,先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類
  • 快取機制,快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效

4.3、雙親委派模型

  雙親委派模型要求除了頂層的啟動類載入器外, 其餘的類載入器都應有自己的父類載入器。 不過這裡類載入器之間的父子關係一般不是以繼承(Inheritance) 的關係來實現的, 而是通常使用組合(Composition) 關係來複用父載入器的程式碼。

4.3.1、雙親委派機制

  如果一個類載入器收到了類載入的請求, 它首先不會自己去嘗試載入這個類, 而是把這個請求委派給父類載入器去完成, 每一個層次的類載入器都是如此, 因此所有的載入請求最終都應該傳送到最頂層的啟動類載入器中, 只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類) 時, 子載入器才會嘗試自己去完成載入

  1. 當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  2. 當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  4. 若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。

4.3.2、雙親委派原始碼分析

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判斷該型別是否已經被載入
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
                try {
                    if (parent != null) {
                         //如果存在父類載入器,就委派給父類載入器載入
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,通過呼叫本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

  先檢查請求載入的型別是否已經被載入過, 若沒有則呼叫父載入器的loadClass()方法, 若父載入器為空則預設使用啟動類載入器作為父載入器。 假如父類載入器載入失敗,丟擲ClassNotFoundException異常的話, 才呼叫自己的findClass()方法嘗試進行載入。

4.3.3、雙親委派的意義

  • 保證Java中的類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,系統類防止記憶體中出現多份同樣的位元組碼
  • 保證Java程式安全穩定執行

  例如類java.lang.Object, 它存放在rt.jar之中, 無論哪一個類載入器要載入這個類, 最終都是委派給處於模型最頂端的啟動類載入器進行載入, 因此Object類在程式的各種類載入器環境中都能夠保證是同一個類。 反之, 如果沒有使用雙親委派模型, 都由各個類載入器自行去載入的話, 如果使用者自己也編寫了一個名為java.lang.Object的類, 並放在程式的ClassPath中, 那系統中就會出現多個不同的Object類, Java型別體系中最基礎的行為也就無從保證, 應用程式將會變得一片混亂。

4.4、自定義類載入器

  通常情況下,我們都是直接使用系統類載入器。但是,有的時候,我們也需要自定義類載入器。比如應用是通過網路來傳輸 Java 類的位元組碼,為保證安全性,這些位元組碼經過了加密處理,這時系統類載入器就無法對其進行載入,這樣則需要自定義類載入器來實現。自定義類載入器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們只需要重寫 findClass 方法即可

import java.io.*;

public class MyClassLoader  extends  ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.neo.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

注意點:

  • 這裡傳遞的檔名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。
  • 最好不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。
  • 這類Test 類本身可以被 AppClassLoader 類載入,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由於雙親委託機制的存在,會直接導致該類由 AppClassLoader 載入,而不會通過我們自定義類載入器來載入

五、java模組化系統

  在JDK 9中引入的Java模組化系統(Java Platform Module System, JPMS) 是對Java技術的一次重要升級, 為了能夠實現模組化的關鍵目標——可配置的封裝隔離機制, Java虛擬機器對類載入架構也做出了相應的變動調整, 才使模組化系統得以順利地運作。 JDK 9的模組不僅僅像之前的JAR包那樣只是簡單地充當程式碼的容器, 除了程式碼外, Java的模組定義還包含以下內容:

  • 依賴其他模組的列表。
  • 匯出的包列表, 即其他模組可以使用的列表。
  • 開放的包列表, 即其他模組可反射訪問模組的列表。
  • 使用的服務列表。
  • 提供服務的實現列表

解決的問題:

  1. 可配置的封裝隔離機制首先要解決JDK 9之前基於類路徑(ClassPath) 來查詢依賴的可靠性問題。 此前, 如果類路徑中缺失了執行時依賴的型別, 那就只能等程式執行到發生該型別的載入、 連結時才會報出執行的異常,如果啟用了模組化進行封裝, 模組就可以宣告對其他模組的顯式依賴, 這樣Java虛擬機器就能夠在啟動時驗證應用程式開發階段設定好的依賴關係在執行期是否完備, 如有缺失那就直接啟動失敗。
  2. 可配置的封裝隔離機制還解決了原來類路徑上跨JAR檔案的public型別的可訪問性問題。 JDK 9中的public型別不再意味著程式的所有地方的程式碼都可以隨意訪問到它們, 模組提供了更精細的可訪問性控制, 必須明確宣告其中哪一些public的型別可以被其他哪一些模組訪問, 這種訪問控制也主要是在類載入過程中完成的

5.1、模組的相容性

  為了使可配置的封裝隔離機制能夠相容傳統的類路徑查詢機制, JDK 9提出了與“類路徑”(ClassPath) 相對應的“模組路徑”(ModulePath) 的概念。 簡單來說, 就是某個類庫到底是模組還是傳統的JAR包, 只取決於它存放在哪種路徑上。 只要是放在類路徑上的JAR檔案, 無論其中是否包含模組化資訊(是否包含了module-info.class檔案) , 它都會被當作傳統的JAR包來對待; 相應地, 只要放在模組路徑上的JAR檔案, 即使沒有使用JMOD字尾, 甚至說其中並不包含module-info.class檔案, 它也仍然會被當作一個模組來對待

  模組化系統將按照以下規則來保證使用傳統類路徑依賴的Java程式可以不經修改地直接執行在JDK 9及以後的Java版本上, 即使這些版本的JDK已經使用模組來封裝了Java SE的標準類庫, 模組化系統的這套規則也仍然保證了傳統程式可以訪問到所有標準類庫模組中匯出的包。·

  1. JAR檔案在類路徑的訪問規則: 所有類路徑下的JAR檔案及其他資原始檔, 都被視為自動打包在一個匿名模組(Unnamed Module) 裡, 這個匿名模組幾乎是沒有任何隔離的, 它可以看到和使用類路徑上所有的包、 JDK系統模組中所有的匯出包, 以及模組路徑上所有模組中匯出的包。
  2. 模組在模組路徑的訪問規則: 模組路徑下的具名模組(Named Module) 只能訪問到它依賴定義中列明依賴的模組和包, 匿名模組裡所有的內容對具名模組來說都是不可見的, 即具名模組看不見傳統JAR包的內容。
  3. ·JAR檔案在模組路徑的訪問規則: 如果把一個傳統的、 不包含模組定義的JAR檔案放置到模組路徑中, 它就會變成一個自動模組(Automatic Module) 。 儘管不包含module-info.class, 但自動模組將預設依賴於整個模組路徑中的所有模組, 因此可以訪問到所有模組匯出的包, 自動模組也預設匯出自己所有的包。

5.2、模組化下的類載入器

首先, 是擴充套件類載入器(Extension Class Loader) 被平臺類載入器(Platform Class Loader) 取代。這其實是一個很順理成章的變動, 既然整個JDK都基於模組化進行構建(原來的rt.jar和tools.jar被拆分成數十個JMOD檔案) , 其中的Java類庫就已天然地滿足了可擴充套件的需求, 那自然無須再保留<JAVA_HOME>\lib\ext目錄, 此前使用這個目錄或者java.ext.dirs系統變數來擴充套件JDK功能的機制已經沒有繼續存在的價值了, 用來載入這部分類庫的擴充套件類載入器也完成了它的歷史使命。

其次, 平臺類載入器和應用程式類載入器都不再派生自java.net.URLClassLoader, 如果有程式直接依賴了這種繼承關係, 或者依賴了URLClassLoader類的特定方法, 現在啟動類載入器、 平臺類載入器、 應用程式類載入器全都繼承於jdk.internal.loader.BuiltinClassLoader, 在BuiltinClassLoader中實現了新的模組化架構下類如何從模組中載入的邏輯, 以及模組中資源可訪問性的處理。變化如下圖所示:

jdk9之前版本的類載入框架

 jdk9以及之後版本的類載入框架

 所以,jdk9以及後面的版本的類載入的委派關係圖如下所示:

 

 

  最後, JDK 9中雖然仍然維持著三層類載入器和雙親委派的架構, 但類載入的委派關係也發生了變動。 當平臺及應用程式類載入器收到類載入請求, 在委派給父載入器載入前, 要先判斷該類是否能夠歸屬到某一個系統模組中, 如果可以找到這樣的歸屬關係, 就要優先委派給負責那個模組的載入器完成載入, 也許這可以算是對雙親委派的第四次破壞

 

參考:

《深入理解java虛擬機器第三版》

https://www.cnblogs.com/ityouknow/p/5603287.html

 

相關文章