1. 程式人生 > 實用技巧 >類載入機制與雙親委派

類載入機制與雙親委派

類載入:虛擬機器將描述類的資料從class 檔案載入到記憶體中,對載入的資料進行驗證,準備,解析,初始化;最後得到虛擬機器認可後轉化成直接可以使用的 java 型別的過程

1、類載入機制

  • 載入
    • 通過一個類的全限定類名來獲取定義此類的二進位制位元組流
    • 將這個位元組流所代表的靜態儲存結構轉化成方法區的執行時資料結構
    • 在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這個類的各種資料訪問入口
  • 驗證(檔案格式,元資料,位元組碼,符號引用等校驗)
    • 確保Class 檔案的位元組流中包含的資訊符合當前JVM的要求,並且不會危害 JVM 自身的安全
    • JVM 如果不檢查輸入的位元組流,對其完全信任,很可能會因為載入了有害的位元組流而導致系統崩潰
    • 校驗是 JVM 對自身保護的一種重要工作
  • 準備(分配記憶體,變數初始值設定)
    • 將為靜態變數申請記憶體,並賦予初始值(基本型別為其預設值,引用型別為 null
    • 真正的賦值操作是在初始化階段
  • 解析
    • JVM 將常量池內的符號引用替換成直接引用的過程
  • 初始化
    • 掉用類的初始化方法<clinit()> 為靜態變數賦予實際的值,執行靜態程式碼塊
    • 對於初始化,JVM 規範嚴格規定了有且只有 5 種情況必須立即對類進行初始化
      • 遇到 newgetstaticputstaticinvokestatic 這四個位元組碼指令時,如果類還沒有進行過初始化,則要先觸發其初始化,使用
        new 關鍵字例項化物件時,讀取或設定一個類的靜態欄位 ( static ) 時,(被static 修飾又被final 修飾的,已在編譯期把結果放入常量池的靜態欄位除外),呼叫一個類的靜態方法時
      • 使用 java.lang.reflect 包的方法對類進行反射呼叫時,如果類還沒有進行過初始化,則需要先觸發其初始化
      • 當初始化一個類的時候,如果發現其父類還未初始化,則需要先觸發其父類的初始化
      • JVM 啟動時,需要指定一個主類(即包含public static void main(String[] args) 方法的類),JVM 會對該主類觸發初始化
      • 當使用 JDK1.5 支援時,如果一個java.lang.invoke.MethodHandler例項最後的解析結果
        REF_getstaticREF_putstaticREF_invokestatic的方法控制代碼時,並且這個方法控制代碼所對應的類沒有進行過初始化,則先需要先觸發其初始化
  • 使用
  • 解除安裝

2、雙親委派模型(避免重複載入 + 避免核心被篡改)

當一個類載入器收到一個類載入請求時,首先把載入任務委託給父類載入器,依次遞迴;

如果父類載入器可以完成類載入任務,就成功返回;

只有當父類載入器無法完成載入任務時,才自己去載入

好處:

  • Java 類隨著它的類載入器具備了一種帶有優先順序的層次關係
  • 通過這種層次關係,可以避免類的重複載入,當父類已經載入了此類時,就沒必要子 ClassLoader 再載入一次
  • 防止重複載入同一個 .class 檔案(自定義同名的 java 檔案),通過委託機制,如果父類載入過了,就不再載入了
  • 考慮到安全因素,Java 核心 API 中定義型別不會被隨意替換
  • 假設通過網路傳遞一個名為 java.lang.Integer 的類,通過雙親委派機制傳遞到最頂層載入器,而頂層載入器在核心 API 中發現這個名字的類,發現該類已被載入,並不會重新載入這個網路傳遞過來的同名的 java.lang.Integer ,而直接返回已載入過的類,這樣可以防止核心 API 被篡改

      

核心載入類的方法:

 1 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 2     synchronized (getClassLoadingLock(name)) {
 3         // First, check if the class has already been loaded
 4         Class<?> c = findLoadedClass(name);
 5         if (c == null) {
 6             long t0 = System.nanoTime();
 7             try {
 8                 if (parent != null) {
 9                  //呼叫父類的 ClassLoader 來載入
10                     c = parent.loadClass(name, false);
11                 } else {
12              // 沒找到父類載入器,查詢最頂層的 BootstrapClassLoader 來載入
13                     c = findBootstrapClassOrNull(name);
14                 }
15             } catch (ClassNotFoundException e) {
16                 // ClassNotFoundException thrown if class not found
17                 // from the non-null parent class loader
18             }
19             if (c == null) {
20                 // If still not found, then invoke findClass in order
21                 // to find the class.
22             // 如果父類載入器都沒找到,就直接呼叫查詢類的方法去查詢
23                 long t1 = System.nanoTime();
24                 c = findClass(name);
25                 // this is the defining class loader; record the stats
26                 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
27                 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
28                 sun.misc.PerfCounter.getFindClasses().increment();
29             }
30         }
31         if (resolve) {
32             resolveClass(c);
33         }
34         return c;
35     }
36 }
  • 每一個 ClassLoader 都會持有一個父類的ClassLoader 物件
  • 注意這裡不是繼承,而是組合關係
  • 當呼叫當前的載入類的方法的時候,其實內部是呼叫父類的ClassLoader 來完成載入
  • 如果最頂層的父類載入器丟擲異常,說明父類無法完成載入請求,此時就由子類來完成查詢類,載入類的過程了