1. 程式人生 > >《深入理解Java虛擬機器》學習筆記之類載入機制

《深入理解Java虛擬機器》學習筆記之類載入機制

一、概述

  • 定義:

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

    • 在Java語言中,型別的載入、連線和初始化過程都是在程式執行期間完成的
      • 優點:高度的靈活性。Java中可以動態擴充套件的語言特性就是依賴 執行期間動態載入和動態連線 這個特點實現的

      • 缺點:類載入時的效能開銷。

二、類載入的時機

1、生命週期
  • 類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括7個階段
    image

    • 這些階段的“開始”時機是按照這個順序

      • 這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中呼叫、啟用另外一個階段
    • 載入 -> 驗證 -> 準備 -> 初始化 -> 解除安裝這五個階段的順序是確定的

    • “解析”階段的開始時機是不確定的,在某些情況下可以在初始化階段之後再開始

      • 這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)
2、初始化的時機
  • 可通過靜態程式碼塊檢查類是否載入

    static {
        ...
    }
    
  • 虛擬機器開始類載入的時機規範中沒有強制約束,但在以下有且只有5種情況下如果類沒有進行初始化,則必須立刻對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始)。

    • (1)遇到newgetstaticputstaticinvokestatic

      這四條位元組碼指令時。生成這四條指令的常見Java程式碼場景是:

      • 1⃣使用new關鍵字例項化物件的時候

      • 2⃣讀取或設定一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)的時候

      • 3⃣呼叫一個類的靜態方法的時候

    • (2)使用java.lang.reflect包的方法對類進行反射呼叫的時候

    • (3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化

      • 介面:當一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如引用介面中定義的常量)才會初始化
    • (4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類)、虛擬機器會先初始化這個主類

    • (5)當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果是REF_getStaticREF_putStaticREF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化

  • 主動引用與被動引用

    • (1)主動引用:上述五種場景中的行為稱為對一個類的主動引用

    • (2)被動引用:除上述情況外,所有引用類的方式都不會觸發初始化,稱為被動引用

    • 被動引用的例子:

      • 1⃣通過子類引用父類的靜態欄位,不會導致子類初始化

      • 2⃣通過陣列定義來引用類,不會觸發此類的初始化

        //不會觸發SuperClass類的初始化
        //因為建立動作是由位元組碼指令 newarray 觸發
        
        SuperClass[] sca = new SuperClass[10];    
        
      • 3⃣常量在編譯階段會存入呼叫類的常量池(常量傳播優化)中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

三、類載入的過程(7個階段)

1、載入
  • 載入階段需要完成的三件事

    • (1)通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等)

    • (2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構

    • (3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區的這個類的各種資料的訪問入口

  • 非陣列類與陣列類的載入(載入階段中獲取類的二進位制位元組流的動作)

    • (1)非陣列類

      • 可以使用系統提供的引導類載入器來完成
      • 可以使用開發人員自定義的類載入器去控制位元組流的獲取方式(即重寫一個類載入器的loadClass()方法)
    • (2)陣列類

      • 本身不通過類載入器建立,而是由Java虛擬機器直接建立
      • 但陣列類的元素型別最終還是要靠類載入器去建立
  • 陣列類建立過程需遵循的規則

    • (1)如果陣列的元件型別(Component Type,指的是陣列去掉一個維度的型別)是引用型別,那就採用普通的載入過程去載入這個元件型別,陣列類將在載入該元件型別的類載入器的類名稱空間上被標識(一個類必須與類載入器一起確定唯一性)

    • (2)如果陣列的元件型別不是引用型別(eg:int[] 陣列),Java虛擬機器將會把原陣列標記為與引導類載入器關聯

    • (3)陣列類的可見性與它的元件型別的可見性一致,如果元件型別不是引用型別,那陣列類的可見性將預設為public

  • 結果

    • 載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中

      • 方法區中的資料儲存格式由虛擬機器實現自行定義,虛擬機器規範未規定此區域的具體資料結構
    • 然後在記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區中的這些型別資料的外部介面

      • 並沒有明確規定是在Java堆中,對於HotSpot虛擬機器而言,Class物件比較特殊,它雖然是物件,但是存放在方法區中
2、驗證
  • 目的:為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全

    • 是虛擬機器對自身保護的一項重要工作
    • 本階段是否嚴謹,直接決定了Java虛擬機器是否能承受惡意程式碼的攻擊
    • 本階段很重要,但不必需(對程式執行期沒有影響)
  • 執行效能:驗證階段的工作量在虛擬機器的類載入子系統中又佔了相當大的一部分

  • 驗證不通過:如果驗證到輸入的位元組流不符合Class檔案格式的約束,虛擬機器就應丟擲一個java.lang.VerifyError異常或其子類異常

  • 過程(四個階段)

    • (1)檔案格式驗證(檢查位元組流)

      • 目的:驗證位元組流是否符合Class檔案格式的規範(保證輸入的位元組流能正確的解析並存儲於方法區之內,格式上符合描述一個Java型別資訊的要求),並能被當前版本的虛擬機器處理

      • 內容:是否以魔數0xCAFEBABE開頭、主次版本號是否在當前虛擬機器處理範圍之內、常量池的常量中是否有不被支援的常量型別…

      • 特點:本階段的驗證是基於二進位制位元組流進行的,通過這個階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存。所以後面的三個驗證階段全部是基於方法區的儲存結構進行的,不會再直接操作位元組流

    • (2)元資料驗證(檢查資料型別)

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

      • 內容:這個類是否有父類、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)、如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法…

    • (3)位元組碼驗證(檢查類的方法體)

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

      • 內容:保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作、保證跳轉指令不會跳轉到方法體以外的位元組碼指令上、保證方法體中的型別轉換是有效的…

      • 將位元組驗證的型別推導轉變為型別檢查:為避免過多的時間消耗在位元組碼驗證階段,給方法體的Code屬性的屬性表中增加了一項名為“StackMapTable”的屬性,這項屬性描述了方法體中所有的基本塊(Basic Block,按照控制流拆分的程式碼塊)開始時本地變量表和操作棧應有的狀態,在位元組碼驗證期間,就不需要根據程式推導這些狀態的合法性,只需要檢查StackMapTable屬性中的記錄是否合法即可

    • (4)符號引用驗證(檢查常量池中的資訊)

      • 發生時機:發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在連線的第三階段——解析階段中發生

      • 目的:對類自身以外(常量池中的各種符號引用)的資訊進行匹配性檢驗,確保解析動作能正常執行

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

      • 結果:如果無法通過符號引用驗證

3、準備
  • 作用:正式為類變數分配記憶體並設定類變數初始值的階段

    • 這些變數所使用的記憶體都將在方法區中進行分配

    • 本階段進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數。例項變數將會在物件例項化時隨著物件一起分配在Java堆中

    • 本階段將會對類變數第一次賦初值

      • 這裡的初值“通常情況”下是資料型別的零值
        //此階段value被賦的初值是預設的int零值,0
        
        public static int value = 123;
        
      • 但如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數就會被初始化為ConstantValue屬性所指定的值
        //被final修飾的類變數含有ConstantValue屬性,因此此階段就已經被賦予了值123
        
        public final static int value = 123;
        
4、解析
  • 作用:虛擬機器將常量池內的符號引用替換為直接引用的過程

    類別 定義 指向目標與記憶體情況
    符號引用 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可 符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中
    直接引用 直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼 直接引用是和虛擬機器實現的記憶體佈局相關的,如果有了直接引用,那引用的目標必定已經在記憶體中存在
  • 發生時機:規範中並未規定解析階段發生的具體時間,只要求在執行以下16個用於操作符號引用的位元組碼指令之前,先對他們所使用的符號引用進行解析

    • anewarray
    • checkcast
    • getfield
    • getstatic
    • instanceof
    • invokedynamic
    • invokeinterface
    • invokespecial
    • invokestatic
    • invokevirtual
    • ldc
    • ldc_w
    • multianewarray
    • new
    • putfield
    • putstatic
  • 對同一個符號引用進行多次解析

    • (1)除invokedynamic指令以外的指令(靜態的):虛擬機器實現可以對第一次解析的結果進行快取(在執行時常量池中記錄直接引用,並把常量標示為已解析狀態),從而避免解析動作重複進行

    • (2)對於invokedynamic指令(動態的):之前解析的結果不一定對後續相同的invokedynamic指令生效,此指令目的就在於動態語言支援,必須等到程式實際執行到這條指令的時候,解析動作才能進行。

  • Java是一門強型別、靜態型別語言

    • 靜態與動態:Static typing when possible, dynamic typing when needed。
    型別 特點 例子
    靜態型別 資料型別是在編譯其間檢查的。在寫程式時要宣告所有變數的資料型別 python、ruby、指令碼語言
    動態型別 在執行期間才去做資料型別檢查。在用動態型別的語言程式設計時,永遠也不用給任何變數指定資料型別,該語言會在你第一次賦值給變數時,在內部將資料型別記錄下來 C#、Java
    • 強型別與弱型別:
    型別 特點 優缺點
    強型別 強制資料型別定義的語言。一旦一個變數被指定了某個資料型別,如果不經過強制轉換,那麼它就永遠是這個資料型別了 編譯速度稍差,型別安全
    弱型別 資料型別可以被忽略的語言。一個變數可以賦不同資料型別的值 編譯速度較快,型別不安全
  • 解析的物件(七類符號引用)

    物件 常量池中的常量型別
    類或介面 CONSTANT_Class_info
    欄位 CONSTANT_Fieldref_info
    類方法 CONTANT_Methodref_info
    介面方法 CONTANT_InterfaceMethordref_info
    方法型別 CONSTANT_MethodType_info
    方法控制代碼 CONSTANT_MethodHandle_info
    呼叫點限定符 CONSTANT_InvokeDynamic_info
    • (1)類或介面的解析(最後需進行許可權驗證)

      • 假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或介面C的直接引用,需要如下三個步驟

      • 1⃣ 如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C

      • 2⃣ 如果C是一個數組型別,並且陣列的元素型別是物件,則會按照1中的規則去載入陣列元素型別,接著由虛擬機器生成一個代表此陣列維度和元素的陣列物件

      • 3⃣ 如果1⃣2⃣沒有出現任何異常,則C在虛擬機器中實際上已經成為一個有效的類或介面了,但在解析完成之前需要進行符號引用驗證,確認D是否具備對C的訪問許可權,如果發現不具備訪問許可權,將丟擲java.lang.IllegalAccessError

    • (2)欄位解析(最後需進行許可權驗證)

      • 要解析一個未被解析過的欄位符號引用,首先會對欄位表內的class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是欄位所屬的類或介面的符號引用。

      • 如果解析成功,將這個欄位所屬的類或介面用C表示,需要如下步驟對C的後續欄位進行搜尋

      • 1⃣ 查自身:如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位。則返回這個欄位的直接引用,查詢結束。

      • 2⃣ 查介面實現:否則,如果在C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接飲用,查詢結束。

      • 3⃣ 查父類繼承:否則,如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。

      • 4⃣ 否則,查詢失敗,丟擲java.lang.NoSuchFieldError異常

    • (3)類方法解析(最後需進行許可權驗證)

      • 要解析一個未被解析過的類方法,首先會對類方法表內的class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是類方法所屬的類或介面的符號引用。

      • 如果解析成功,使用C表示這個類方法所屬的類,按如下步驟進行後續類方法搜尋

      • 1⃣ 查是否是介面:類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法中發現class_index中索引的C是個介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常

      • 2⃣ 查自身:如果確認該C為一個類,則在類C中查詢是否有簡單名稱和描述符鬥魚目標相匹配的方法,如果有則返回這個方法的直接引用

      • 3⃣ 查父類:否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束

      • 4⃣ 查是否為抽象類:否則,在類C實現的介面列表及他們的父介面中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,查詢結束,丟擲java.lang.AbstractMethodError異常

      • 5⃣ 否則,宣告方法查詢失敗,丟擲java.lang.IllegalAccessError

    • (4)介面方法解析(無需許可權驗證,介面方法預設public)

      • 要解析一個未被解析過的介面方法,首先會對介面方法表內的class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是介面方法所屬的類或介面的符號引用。

      • 如果解析成功,使用C表示這個介面方法所屬的介面,按如下步驟進行後續介面方法搜尋

      • 1⃣ 檢查是否是類:如果在介面方法表中發現class_index中的索引C是個類而不是介面,那就直接丟擲java.lang.IncompatibleClassChangeError

      • 2⃣ 查自身:否則,在介面C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束

      • 3⃣ 查父介面:否則,在介面C的父介面中遞迴查詢,直到java.lang.Object(查詢範圍包括Object類)類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束

      • 4⃣ 否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError

5、初始化
  • 作用:開始真正執行類中定義的Java程式程式碼(或者說位元組碼)

    • 在準備階段,變數已賦過一次系統要求的初值

    • 初始化階段,是根據程式設計師通過程式指定的主觀計劃去初始化類變數和其他資源

      • 初始化階段是執行類構造器<clinit>()方法的過程
  • <clinit>()方法執行過程的特點

    • (1)<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的。

      • 編譯器收集的順序是由語句在原始檔中出現的順序所決定的
      • 靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問
      public class Test {
          static {
              i = 0;                        //給變數賦值可以正常編譯通過
              System.out.print(i);          //這句編譯器會提示“非法的向前引用”
          }
          static int i = 1;
      }
      
    • (2)<clinit>()方法與類的建構函式(或者說例項構造器<init>()方法)不同,它不需要顯式的呼叫父類構造器(super),虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢

      • 由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作
    • (3)<clinit>()方法對於類或介面來說並不是必需的。

      • 如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法
    • (4)介面與類一樣都會生成<clinit>()方法。

      • 原因:介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作

      • 介面與類的區別1⃣ :執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。

      • 介面與類的區別2⃣ :介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法

    • (5)虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步

      • 如果有多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢

      • 如果<clinit>()方法中有耗時很長的操作,可能造成多個程序阻塞。

      • 如果存線上程阻塞的情況,當執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。因為同一個類載入器下,一個型別只會初始化一次。

四、類載入器

  • 定義:在類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組稱為“類載入器”。
1、類與類載入器
  • 判定類是否“相等”

    • 對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間

    • “相等”:代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做物件所屬關係判定等情況

    • 比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等

  • 從Java虛擬機器的角度,類載入器有2種

    • (1)啟動類載入器(Bootstrap ClassLoader)

      • 使用C++語言實現,是虛擬機器自身的一部分
    • (2)所有其他的類載入器

      • 由Java語言實現,獨立於虛擬機器外部
      • 都繼承自抽象類java.lang.ClassLoader
  • 從Java開發人員角度,類載入器有3種

    • (1)啟動類載入器(Bootstrap ClassLoader)

      • 使用方式:無法被Java程式直接引用。使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,那直接使用null代替即可

        public ClassLoader getClassLoader() {
            ClassLoader cl = getClassLoader();
            if (cl == null) {
                return null;
            }
        }
        
      • 作用:負責將存放在<JAVA_HOME>\lib目錄中的、並且是被虛擬機器識別的(僅按照檔名識別)類庫載入到虛擬機器記憶體中

    • (2)擴充套件類載入器(Extension ClassLoader)

      • 使用方式:開發者可直接使用

      • 作用:由sun.misc.Launcher$ExtClassLoader實現,負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫

    • (3)應用程式類載入器(Application ClassLoader)

      • 別名:系統類載入器

      • 使用方式:開發者可以直接使用。如果引用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器

      • 作用:由sun.misc.Launcher$AppClassLoader實現。是ClassLoader中的getSystemClassLoader()方法的返回值,負責載入使用者路徑(ClassPath)上所指定的類庫

2、雙親委派模型

image

  • 要求

    • 類載入器存在層次關係
    • 除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器
  • 工作過程

    • 如果一個類載入器收到了類載入請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成

    • 每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中

    • 只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入

  • 好處

    • Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係

      • 實現了基類能總是被同一個類載入器載入,確保唯一性
    • eg:類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類

  • 邏輯

    • 位置:實現雙親委派的程式碼集中在java.lang.ClassLoaderloadClass()方法中

    • (1)檢查是否被載入過

    • (2)若沒有載入則呼叫父載入器的loadClass()

      • 若父載入器為空,則預設使用啟動類載入器作為父載入器
    • (3)如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的finaClass()方法進行載入

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //呼叫父類的類載入器
                        c = parent.loadClass(name, false);
                    } else {
                        //父類載入器為空,呼叫啟動類載入器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
    
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    
                    //呼叫自己的findClass()載入
                    c = findClass(name);
    
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    
  • 沙箱機制

    • 沙箱機制是由基於雙親委派機制上 採取的一種JVM的自我保護機制
    • 假設你要寫一個java.lang.String 的類,由於雙親委派機制的原理,此請求會先交給Bootstrap試圖進行載入,但是Bootstrap在載入類時首先通過包和類名查詢rt.jar中有沒有該類,有則優先載入rt.jar包中的類,因此就保證了java的執行機制不會被破壞.
3、破壞雙親委派模型
  • 第一次被破壞

    • 原因:為了向前相容

      • 雙親委派模型是JDK1.2之後才被引入,而ClassLoader在JDK1.0就已存在
    • JDK1.2後,不提倡使用者覆蓋loadClass()方法,而應當把自己的類載入邏輯寫到findClass()方法中

      • loadClass()方法的邏輯例如果父類載入失敗,則會呼叫自己的findClass()方法,從而確保新寫的類載入器是符合雙親委派規則的。
  • 第二次被破壞

    • 原因:雙親委派模型自身的缺陷

    • 問題:基礎類要回呼叫戶的程式碼(eg:JNDI服務對資源進行集中管理和查詢)

    • 解決:引入執行緒上下文類載入器(Thread Context ClassLoader)

      • 違背了雙親委派機制,使父類載入器請求子類載入器去完成類載入

      • 這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定

      • 如果建立執行緒時還未設定,它將會從父執行緒中繼承一個

      • 如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器

  • 第三次被破壞

    • 原因:使用者對程式動態性的追求

      • 動態性:程式碼熱替換(HotSwap)、模組熱部署(Hot Deployment)
    • 目前OSGi成為業界事實上的Java模組化標準

      • OSGi模組化熱部署實現關鍵 -> 自定義的類載入器機制的實現:當需要更換一個Bundle時,就把Bundle連同類載入器一同換掉以實現程式碼的熱替換
    • 在OSGi環境下,類載入器不再是雙進委派模型中的樹狀結構,而是進一步發展為更加複雜的網狀結構。當收到類載入請求時,OSGi將按照下面的順序進行類搜尋(1⃣2⃣:雙親委派,其它:平級類載入器中的類查詢)

      • 1⃣ 將以java.*開頭的類委派給父類載入器載入

      • 2⃣ 否則,將委派列表名單內的類委派給父類載入器載入

      • 3⃣ 否則,將Import列表中的類委派給Export這個類的Bundle的類載入器載入

      • 4⃣ 否則,查詢當前Bundle的ClassPath,使用自己的類載入器載入

      • 5⃣ 否則,查詢類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類載入器載入

      • 6⃣ 否則,查詢Dynamic Import列表的Bundle,委派給對應Bundle的類載入器載入

      • 7⃣ 否則,類查詢失敗