天天看點

第7章-虛拟機類加載機制

文章目錄

    • 7.1 概述
    • 7.2 類加載的時機
      • 7.2.1 生命周期
      • 7.2.2 加載時間點
    • 7.3 類加載的過程
      • 7.3.1 加載
      • 7.3.2 驗證
        • 7.3.2.1 檔案格式驗證
        • 7.3.2.2 中繼資料驗證
        • 7.3.2.3 位元組碼驗證
        • 7.3.2.4 符号引用驗證
      • 7.3.3 準備
      • 7.3.4 解析
        • 7.3.4.1 類或接口的解析
        • 7.3.4.2 字段解析
        • 7.3.4.3 類方法解析
        • 7.3.4.4 接口方法解析
      • 7.3.5 初始化
    • 7.4 類加載器
      • 7.4.1 類與類加載器
      • 7.4.2 雙親委派模型

7.1 概述

  1. 類加載機制:虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的 Java 類型。
  2. 與那些在編譯時需要進行連接配接工作的語言不同,在 Java 語言裡面,類型的加載、連接配接和初始化過程都是在程式運作期間完成的,這種政策雖然會令類加載時稍微增加一些性能開銷,但是會為 Java 應用程式提供高度的靈活性,Java裡天生可以動态擴充的語言特性就是依賴運作期動态加載和動态連接配接這個特點實作的。

7.2 類加載的時機

7.2.1 生命周期

  1. 類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連接配接(Linking),這7個階段的發生順序如下:
第7章-虛拟機類加載機制
  1. 其中加載、驗證、準備、初始化和解除安裝這5個階段的順序是确定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的運作時綁定(也稱為動态綁定或晚期綁定)。
  2. 注意,這裡寫的是按部就班地 “開始” ,而不是按部就班地 “進行” 或 “完成”,強調這點是因為這些階段通常都是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段。

7.2.2 加載時間點

  1. 什麼情況下需要開始類加載過程的第一個階段:加載?Java虛拟機規範中并沒有進行強制限制,這點可以交給虛拟機的具體實作來自由把握。但是對于初始化階段,虛拟機規範則是嚴格規定了有且隻有5種情況必須立即對類進行 “初始化”(而加載、驗證、準備自然需要在此之前開始):
    • 遇到 new、getstatic、putstatic 或 invokestatic 這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的 Java代碼場景是:
      • 使用new關鍵字執行個體化對象
      • 讀取或設定一個類的靜态字段(被final修飾、已在編譯期把結果放人常量池的靜态字段除外)
      • 以及調用一個類的靜态方法的時候。
    • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
    • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
    • 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main方法的那個類),虛拟機會先初始化這個主類。
    • 當使用 JDK1.7 的動态語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
  2. 以上 5 種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用,具體例子有:
    • 通過子類引用父類的靜态字段,不會導緻子類初始化
    • 通過數組定義來引用類,不會觸發此類的初始化
    • 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不會觸發定義常量的類的初始化。
  3. 接口的加載過程與類加載過程稍有一些不同:
    • 接口也有初始化過程,這點與類是一緻的,接口中雖然不能使用 “static{}” 語句塊,但編譯器仍然會為接口生成

      <clinit>()

      類構造器,用于初始化接口中所定義的成員變量。
    • 接口與類真正有所差別的是:當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,并不要求其父接口全部都完成了初始化,隻有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。

7.3 類加載的過程

7.3.1 加載

  1. 在加載階段,虛拟機需要完成以下3件事情:
    • 通過一個類的全限定名來擷取定義此類的二進制位元組流。
    • 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
    • 在記憶體中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種資料的通路入口。
  2. 相對于類加載過程的其他階段,一個非數組類的加載階段(準确地說,是加載階段中擷取類的二進制位元組流的動作)是開發人員可控性最強的,因為加載階段既可以使用系統提供的引導類加載器來完成,也可以由使用者自定義的類加載器去完成,開發人員可以通過定義自己的類加載器去控制位元組流的擷取方式(即重寫一個類加載器的

    loadClass()

    方法)。
  3. 對于數組類而言,情況就有所不同,數組類本身不通過類加載器建立,它是由 Java 虛拟機直接建立的。但數組類與類加載器仍然有很密切的關系,因為數組類的元素類型(Element Type,指的是數組去掉所有次元的類型)最終是要靠類加載器去建立,一個數組類(下面簡稱為C)建立過程就遵循以下規則:
    • 如果數組的元件類型(Component Type,指的是數組去掉一個次元的類型)是引用類型,那就遞歸采用本節中定義的加載過程去加載這個元件類型,數組 C 将在加載該元件類型的類加載器的類名稱空間上被辨別
    • 如果數組的元件類型不是引用類型(例如 int[] 數組),Java虛拟機将會把數組 C 标記為與引導類加載器關聯。
    • 數組類的可見性與它的元件類型的可見性一緻,如果元件類型不是引用類型,那數組類的可見性将預設為 public。
  4. 加載階段與連接配接階段的部分内容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,加載階段尚未完成,連接配接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬于連接配接階段的内容,這兩個階段的開始時間仍然保持着固定的先後順序。

7.3.2 驗證

  1. 驗證是連接配接階段的第一步,這一階段的目的是為了確定 Class 檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。
  2. 因為 Class 檔案并不一定要求用 Java 源碼編譯而來,可以使用任何途徑産生,甚至包括用十六進制編輯器直接編寫來産生Class檔案。在位元組碼語言層面上,上述 Java 代碼無法做到的事情都是可以實作的,至少語義上是可以表達出來的。虛拟機如果不檢查輸入的位元組流,對其完全信任的話,很可能會因為載入了有害的位元組流而導緻系統崩潰,是以驗證是虛拟機對自身保護的一項重要工作。
  3. 從執行性能的角度上講,驗證階段的工作量在虛拟機的類加載子系統中占了相當大的一部分。從整體上看,驗證階段大緻上會完成下面4個階段的檢驗動作:
    • 檔案格式驗證
    • 中繼資料驗證
    • 位元組碼驗證
    • 符号引用驗證

7.3.2.1 檔案格式驗證

  1. 第一階段要驗證位元組流是否符合Class檔案格式的規範,并且能被目前版本的虛拟機處理。這一階段可能包括下面這些驗證點:
    • 是否以魔數 0xCAFEBABE 開頭。
    • 主、次版本号是否在目前虛拟機處理範圍之内。
    • 常量池的常量中是否有不被支援的常量類型(檢查常量tag标志)
    • 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料。
    • Class 檔案中各個部分及檔案本身是否有被删除的或附加的其他資訊。
  2. 實際上,第一階段的驗證點還遠不止這些,該驗證階段的主要目的是保證輸入的位元組流能正确地解析并存儲于方法區之内,格式上符合描述一個 Java 類型資訊的要求。
  3. 這階段的驗證是基于二進制位元組流進行的,隻有通過了這個階段的驗證後位元組流才會進人記憶體的方法區中進行存儲。是以後面的3個驗證階段全部是基于方法區的存儲結構進行的,不會再直接操作位元組流。

7.3.2.2 中繼資料驗證

  1. 第二階段是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 Java 語言規範的要求,這個階段可能包括的驗證點如下:
    • 這類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)。
    • 這類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)。
    • 如果這個類不是抽象類,是否實作了其父類或接口之中要求實作的所有方法。
    • 類中的字段、方法是否與父類産生沖突(例如覆寫了父類的 final 字段,或者出現不符合規則的方法重載,例如方法參數都一緻,但傳回值類型卻不同等)。
  2. 第二階段的主要目的是對類的中繼資料資訊進行語義校驗,保證不存在不符合 Java 語言規範的中繼資料資訊。

7.3.2.3 位元組碼驗證

  1. 第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。
  2. 在第二階段對中繼資料資訊中的資料類型做完校驗後,這個階段将對類的方法體進行校驗分析,保證被校驗類的方法在運作時不會做出危害虛拟機安全的事件:
    • 保證任意時刻操作數棧的資料類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作棧放置了一個int類型的資料,使用時卻按long類型來加載入本地變量表中。
    • 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。、
    • 保證方法體中的類型轉換是有效的
  3. 如果一個類方法體的位元組碼沒有通過位元組碼驗證,那肯定是有問題的;但如果一個方法體通過了位元組碼驗證,也不能說明其一定就是安全的。這裡涉及了離散數學中一個很著名的問題 “Halting Problem”:通俗一點的說法就是,通過程式去校驗程式邏輯是無法做到絕對準确的——不能通過程式準确地檢查出程式是否能在有限的時間之内結束運作。
  4. 由于資料流驗證的高複雜性,在 JDK1.6 之後的 Javac 編譯器和 Java 虛拟機中進行了一項優化,給方法體的 Code 屬性的屬性表中增加了一項名為 “StackMapTable” 的屬性,這項屬性描述了方法體中所有的基本塊(Basic Block,按照控制流拆分的代碼塊)開始時本地變量表和操作棧應有的狀态,在位元組碼驗證期間,就不需要根據程式推導這些狀态的合法性,隻需要檢查 StackMapTable 屬性中的記錄是否合法即可。

7.3.2.4 符号引用驗證

  1. 最後一個階段的校驗發生在虛拟機将符号引用轉化為直接引用的時候,這個轉化動作将在連接配接的第三階段——解析階段中發生。符号引用驗證可以看做是對類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,通常需要校驗下列内容:
    • 符号引用中通過字元串描述的全限定名是否能找到對應的類。
    • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
    • 符号引用中的類、字段、方法的通路性(private、protected、public、default)是否可被目前類通路。
  2. 符号引用驗證的目的是確定解析動作能正常執行,如果無法通過符号引用驗證,那麼将會抛出一個

    java.lang.IncompatibleClassChangeError

    異常的子類
  3. 對于虛拟機的類加載機制來說,驗證階段是一個非常重要的、但不是一定必要(因為對程式運作期沒有影響)的階段。如果所運作的全部代碼(包括自己編寫的及第三方包中的代碼)都已經被反複使用和驗證過,那麼在實施階段就可以考慮使用

    -Xverify:none

    參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。

7.3.3 準備

  1. 準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都将在方法區中進行配置設定。這個階段中有兩個容易産生混淆的概念需要強調一下:
  2. 首先,這時候進行記憶體配置設定的僅包括類變量(被static修飾的變量),而不包括執行個體變量,執行個體變量将會在對象執行個體化時随着對象一起配置設定在Java堆中。
  3. 其次,這裡所說的初始值 “通常情況” 下是資料類型的零值,假設一個類變量的定義為:
public static int value = 123;
           
  1. 那麼變量 value 在準備階段過後的初始值為 0 而不是123,因為這時候尚未開始執行任何 Java方法,而把value指派為123的 putstatic 指令是程式被編譯後,存放于類構造器

    <clinit>()

    方法之中,是以把 value 指派為123的動作将在初始化階段才會執行。
  2. 上面提到,在 “通常情況” 下初始值是零值,那相對的會有一些 “特殊情況”:如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量 value 就會被初始化為ConstantValue 屬性所指定的值,假設上面類變量value的定義變為:
public static final int value = 123;
           
  1. 編譯時 Javac 将會為 value 生成ConstantValue屬性,在準備階段虛拟機就會根據ConstantValue的設定将value指派為123。
資料類型 零值
int
long 0L
short (short) 0
char ‘\u0000’
byte (byte) 0
boolean false
float 0.0f
double 0.0d
reference null

7.3.4 解析

  1. 解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。
    • 符号引用(Symbolic References):符号引用以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,引用的目标并不一定已經加載到記憶體中。各種虛拟機實作的記憶體布局可以各不相同,但是它們能接受的符号引用必須都是一緻的,因為符号引用的字面量形式明确定義在 Java 虛拟機規範的Class檔案格式中。
    • 直接引用(Direct References):直接引用可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是和虛拟機實作的記憶體布局相關的,同一個符号引用在不同虛拟機執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目标必定已經在記憶體中存在。
  2. 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行,分别對應于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info 和 CONSTANT_InvokeDynamic_info 7 種常量類型。下面将講解前面4種引用的解析過程。

7.3.4.1 類或接口的解析

  1. 假設目前代碼所處的類為D,如果要把一個從未解析過的符号引用 N 解析為一個類或接口 C 的直接引用,那虛拟機完成整個解析的過程需要以下3個步驟:
    • 如果C不是一個數組類型,那虛拟機将會把代表 N 的全限定名傳遞給 D 的類加載器去加載這個類 C。在加載過程中,由于中繼資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的接口。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。
    • 如果C是一個數組類型,并且數組的元素類型為對象,也就是N的描述符會是類似

      [Ljava/lang/Integer

      的形式,那将會按照第1點的規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載的元素類型就是

      java.lang.Integer

      ,接着由虛拟機生成一個代表此數組次元和元素的數組對象。
    • 如果上面的步驟沒有出現任何異常,那麼 C 在虛拟機中實際上已經成為一個有效的類或接口了,但在解析完成之前還要進行符号引用驗證,确認 D 是否具備對 C 的通路權限。

7.3.4.2 字段解析

  1. 要解析一個未被解析過的字段符号引用,首先将會解析字段所屬的類或接口的符号引用。如果解析成功完成,那将這個字段所屬的類或接口用 C 表示,虛拟機規範要求按照如下步驟對 C 進行後續字段的搜尋。
    • 如果 C 本身就包含了簡單名稱和字段描述符都與目标相比對的字段,則傳回這個字段的直接引用,查找結束。
    • 否則,如果在C中實作了接口,将會按照繼承關系從下往上遞歸搜尋各個接口和它的父接口
    • 否則,如果C不是 java.lang.Object 的話,将會按照繼承關系從下往上遞歸搜尋其父類
    • 否則,查找失敗,抛出 java.lang.NoSuchFieldError 異常。
  2. 在實際應用中,虛拟機的編譯器實作可能會比上述規範要求得更加嚴格一些,如果有一個同名字段同時出現在 C 的接口和父類中,或者同時在自己或父類的多個接口中出現,那編譯器将可能拒絕編譯。在如下代碼中,如果注釋了Sub類中的

    public static int A = 4;

    ,接口與父類同時存在字段A,那編譯器将提示

    The field Sub.A is ambiguous

    ,并且拒絕編譯這段代碼。
public class FieldResolution {

	interface Interface0 {
		int A = 0;
	}

	interface Interface1 extends Interface0 {
		int A = 1;
	}

	interface Interface2 {
		int A = 2;
	}

	static class Parent implements Interface1 {
		public static int A = 3;
	}

	static class Sub extends Parent implements Interface2 {
		// 注釋該行則會編譯錯誤
        public static int A = 4;
	}

	public static void main(String[] args) {
		System.out.println(Sub.A);
	}
}
           

7.3.4.3 類方法解析

  1. 類方法解析的第一個步驟與字段解析一樣,也需要先解析出方法所屬的類或接口的符号引用,如果解析成功,我們依然用 C 表示這個類,接下來虛拟機将會按照如下步驟進行後續的類方法搜尋:
    • 類方法和接口方法符号引用的常量類型定義是分開的,如果在類方法表中索引的 C 是個接口,那就直接抛出 java.lang.IncompatibleClassChangeError 異常。
    • 如果通過了第1步,在類 C 中查找是否有簡單名稱和描述符都與目标相比對的方法
    • 否則,在類 C 的父類中遞歸查找是否有簡單名稱和描述符都與目标相比對的方法
    • 否則,在類 C 實作的接口清單及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目标相比對的方法,如果存在比對的方法,說明類 C 是一個抽象類,抛出

      java.lang.AbstractMethodError

      異常。
    • 否則,宣告方法查找失敗,抛出 java.lang.NoSuchMethodError。

7.3.4.4 接口方法解析

  1. 接口方法也需要先解析出方法所屬的類或接口的符号引用,如果解析成功,依然用 C 表示這個接口,接下來虛拟機将會按照如下步驟進行後續的接口方法搜尋:
    • 與類方法解析不同,如果在接口方法表中發現索引C是個類而不是接口,那就直接抛出 java.lang.IncompatibleClassChangeEror 異常。
    • 否則,在接口 C 中查找是否有簡單名稱和描述符都與目标相比對的方法
    • 否則,在接口 C 的父接口中遞歸查找,直到 java.lang.Object類(查找範圍會包括Object類)為止
    • 否則,宣告方法查找失敗,抛出 java.lang.NoSuchMethodError 異常。

7.3.5 初始化

  1. 類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘動作完全由虛拟機主導和控制。到了初始化階段,才真正開始執行類中定義的 Java程式代碼(或者說是位元組碼)。
  2. 在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程式員通過程式制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器

    <clinit>()

    方法的過程。
  3. <clinit>()

    方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊(static{}塊)中的語句合并産生的,編譯器收集的順序是由語句在源檔案中出現的順序所決定的
  4. 靜态語句塊中隻能通路到定義在靜态語句塊之前的變量,定義在它之後的變量,在前面的靜态語句塊可以指派,但是不能通路。
public class Test
{
    static
    {
        i = 0;  //  給變量複制可以正常編譯通過
        // System.out.print(i); 這句編譯器會提示 "非法向前引用" (Illegal forward reference)
    }
    static int i = 1;
}
           
  1. <clinit>()

    方法與類的構造函數(或者說執行個體構造器

    <init>()

    方法)不同,它不需要顯式地調用父類構造器,虛拟機會保證在子類的

    <clinit>()

    方法執行之前,父類的

    <clinit>()

    方法已經執行完畢。是以在虛拟機中第一個被執行的

    <clinit>()

    方法的類肯定是 java.lang.Object。
  2. 由于父類的

    <clinit>()

    方法先執行,也就意味着父類中定義的靜态語句塊要優先于子類的變量指派操作。
  3. 在多線程環境下,如果多個線程同時去初始化一個類,那麼隻會有一個線程去執行這個類的

    <clinit>()

    方法,其他線程都需要阻塞等待。但是如果執行

    <clinit>()

    方法的那條線程退出後,其他線程喚醒之後不會再次進入

    <clinit>()

    方法。同一個類加載器下,一個類型隻會初始化一次。

7.4 類加載器

  • 虛拟機設計團隊把類加載階段中的 “通過一個類的全限定名來擷取描述此類的二進制位元組流” 這個動作放到 Java 虛拟機外部去實作,以便讓應用程式自己決定如何去擷取所需要的類。實作這個動作的代碼子產品稱為 “類加載器”。

7.4.1 類與類加載器

  1. 對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立其在 Java 虛拟機中的唯一性。
  2. 每一個類加載器,都擁有一個獨立的類名稱空間。是以比較兩個類是否 “相等”,隻有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class檔案,被同一個虛拟機加載,隻要加載它們的類加載器不同,那這兩個類就必定不相等。
  3. 示例代碼
public class ClassLoaderTest
  {
    public static void main(String[] args) throws Exception
    {
        // 自定義類加載器
        ClassLoader myLoader = new ClassLoader()
        {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException
            {
                try
                {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null)
                    {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                }
                catch (IOException e)
                {
                    throw new ClassNotFoundException(name);
                }
            }
        };
  
        // 通過自定義類加載器加載
        Object obj = myLoader.loadClass("P228_ClassLoad.ClassLoaderTest").newInstance();
  
        // 通過系統類加載器加載
        System.out.println(obj.getClass());
  
        // 由于加載器不同,是以傳回false
        System.out.println(obj instanceof P228_ClassLoad.ClassLoaderTest);
    }
  }
  

運作結果:
  class P228_ClassLoad.ClassLoaderTest
  false
           
  1. 我們使用自定義類加載器去加載了一個名為

    P228_ClassLoad.ClassLoaderTest

    的類,并執行個體化了這個類的對象。兩行輸出結果中,從第一句可以看出,這個對象确實是類

    P228_ClassLoad.ClassLoaderTest

    執行個體化出來的對象,但從第二句可以發現,這個對象在做所屬類型檢查的時候卻傳回了 false。
  2. 這是因為虛拟機中存在了兩個ClassLoaderTest類,一個是由系統應用程式類加載器加載的,另外一個是由我們自定義的類加載器加載的,雖然都來自同一個Class檔案,但依然是兩個獨立的類,做對象所屬類型檢查時結果自然為 false。

7.4.2 雙親委派模型

  1. 從 Java 虛拟機的角度來講,隻存在兩種不同的類加載器:
    • 一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用 C++ 語言實作,是虛拟機自身的一部分
    • 另一種就是所有其他的類加載器,這些類加載器都由 Java 語言實作,獨立于虛拟機外部,并且全都繼承自抽象類 java.lang.ClassLoader。
  2. 從 Java 開發人員的角度來看,類加載器還可以劃分得更細緻一些,絕大部分 Java 程式都會使用到以下3種系統提供的類加載器:
    • 啟動類加載器(Bootstrap ClasLoader):負責将存放在

      <JAVA_HOME>\lib

      目錄中的,或者被

      -Xbootclasspath

      參數所指定的路徑中的,并且是虛拟機識别的類庫加載到虛拟機記憶體中
    • 擴充類加載器(Extension ClassLoader):負責加載

      <JAVA_HOME>\lib\ext

      目錄下的,或者被

      java.ext.dirs

      系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴充類加載器
    • 應用程式類加載器(Application ClassLoader):負責加載使用者類路徑(ClassPath)上所指定的類庫,一般情況下這個就是程式中預設的類加載器。
  3. 下圖展示的類加載器之間的這種層次關系,稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裡類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實作,而是都使用組合(Composition)關系來複用父加載器的代碼。
第7章-虛拟機類加載機制
  1. 雙親委派模型的工作過程是:
    • 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都應該傳送到頂層的啟動類加載器中
    • 隻有當父加載器回報自己無法完成這個加載請求(它的搜尋範圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
  2. 使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是 Java 類随着它的類加載器一起具備了一種帶有優先級的層次關系。例如類 java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,是以 Object 類在程式的各種類加載器環境中都是同一個類。
  3. 相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果使用者自己編寫了一個稱為java.lang.Object 的類,并放在程式的 ClassPath 中,那系統中将會出現多個不同的 Object 類,Java類型體系中最基礎的行為也就無法保證,應用程式也将會變得一片混亂。

繼續閱讀