面試官:如何打破雙親委派機制?

程序员世杰發表於2024-07-05

面試連環call

  1. 雙親委派機制是什麼?如何打破雙親委派機制?
  2. JVM都有哪些類載入器
  3. 如何構造一個自定義類載入器
  4. Tomcat的類載入機制?Spring的類載入機制
  5. Class.forName()和ClassLoader.loadClass()區別?

在開始講述之前簡單回顧一下之前的類載入過程

  • 類載入過程:載入->連線->初始化
  • 其中連線過程又分為:驗證->準備->解析

具體內容大家可以看我上一篇關於類載入過程的詳細介紹 Java類是如何被載入到記憶體中的?

類載入器作用

類載入器的主要作用就是載入 Java 類的位元組碼( .class 檔案)到 JVM 中(在記憶體中生成一個代表該類的 Class 物件)。

位元組碼可以是 Java 源程式(.java檔案)經過 javac 編譯得來,也可以是透過工具動態生成或者透過網路下載得來。

image-20211030235925336

需要注意的是

  • 類載入器是一個負責載入類的物件,用於實現類載入過程中的載入這一步。

  • 每個 Java 類都有一個引用指向載入它的 ClassLoader

  • 陣列類不是透過 ClassLoader 建立的(陣列類沒有對應的二進位制位元組流),是由 JVM 直接生成的。

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

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

類載入器分類

  1. 啟動類載入器: Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。

  2. 擴充套件類載入器: Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。

  3. 應用程式類載入器: Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。

雙親委派模型

🌈 擴充一下:

  • rt.jar:rt 代表“RunTime”,rt.jar是 Java 基礎類庫,包含 Java doc 裡面看到的所有的類的類檔案。也就是說,我們常用內建庫 java.xxx.*都在裡面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。
  • Java 9 引入了模組系統,並且略微更改了上述的類載入器。擴充套件類載入器被改名為平臺類載入器(platform class loader)。Java SE 中除了少數幾個關鍵模組,比如說 java.base 是由啟動類載入器載入之外,其他的模組均由平臺類載入器所載入。

雙親委派機制

定義

如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終會傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子類載入器才會嘗試自己去載入該類。

這種類載入器之間的層次關係被稱為類載入器的“雙親委派模型(Parents Delegation Model)”。

在這裡插入圖片描述

為什麼要使用?

  1. 防止記憶體中出現多份同樣的位元組碼。如果沒有該機制而是由各個類載入器自行載入的話,使用者編寫了一個java.lang.Object的同名類並放在ClassPath中,多個類載入器都能載入這個類到記憶體中,系統中將會出現多個不同的Object類,那麼類之間的比較結果及類的唯一性將無法保證,同時,也會給虛擬機器的安全帶來隱患。
  2. 雙親委派機制能夠保證多載入器載入某個類時,最終都是由一個載入器載入,確保最終載入結果相同
  3. 這樣可以保證系統庫優先載入,即便是自己重寫,也總是使用Java系統提供的System,自己寫的System類根本沒有機會得到載入,從而保證安全性

注意 ⚠️:雙親委派模型並不是一種強制性的約束,只是 JDK 官方推薦的一種方式。如果我們因為某些特殊需求想要打破雙親委派模型

執行流程

雙親委派模型的實現程式碼非常簡單,邏輯非常清晰,都集中在 java.lang.ClassLoaderloadClass() 中,相關程式碼如下所示。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,檢查該類是否已經載入過
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 為 null,則說明該類沒有被載入過
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //當父類的載入器不為空,則透過父類的loadClass來載入該類
                    c = parent.loadClass(name, false);
                } else {
                    //當父類的載入器為空,則呼叫啟動類載入器來載入該類
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父類的類載入器無法找到相應的類,則丟擲異常
            }

            if (c == null) {
                //當父類載入器無法載入時,則呼叫findClass方法來載入該類
                //使用者可透過覆寫該方法,來自定義類載入器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用於統計類載入器相關的資訊
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //對類進行link操作
            resolveClass(c);
        }
        return c;
    }
}

自定義類載入器

構造自定義載入器,需要繼承 ClassLoader 。如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類載入器載入的類最終會透過這個方法被載入。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法。

不破壞雙親委派

實現自定義類載入器的實現,主要分三個步驟

  • 建立一個類繼承ClassLoader抽象類
  • 重寫findClass()方法
  • 在findClass()方法中呼叫defineClass()
public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = getClassBytes(name);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
  
    private byte[] getClassBytes(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
}

破壞雙親委派

定義CoderWorldClass.java檔案,並使用javac編譯成.class檔案

public class CoderWorldClass {
  public CoderWorldClass(){
     System.out.println("CoderWorldClass:"+getClass().getClassLoader());
     System.out.println("CoderWorldClass Parent:"+getClass().getClassLoader().getParent());
  }
  public String print(){
    System.out.println("CoderWorldClass method for print NEW");  //修改了列印語句,用來區分被載入的類
    return "CoderWorldClass.print()";
  }
}

在MyClassLoader類中,重寫loadClass方法,程式碼如下

@Override
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) {
      // If still not found, then invoke findClass in order
      // to find the class.
      long t1 = System.nanoTime();

      //非自定義的類還是走雙親委派載入
      if (!name.equals("CoderWorldClass")) { 
        c = this.getParent().loadClass(name);
      } else { //自己寫的類,走自己的類載入器。
        c = findClass(name);
      }
      // this is the defining class loader; record the stats
      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
      sun.misc.PerfCounter.getFindClasses().increment();
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
}

透過重寫loadClass方法,使得自己建立的類,讓第一個載入器直接載入,不委託父載入器尋找,從而實現雙親委派的破壞

Tomcat類載入

Tomcat是如何實現應用jar包的隔離的?

在思考這個問題之前,我們先來想想Tomcat作為一個JSP/Servlet容器,它應該要解決什麼問題?

  1. 一個web容器需要部署多個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器只有一份,因此要保證每個應用程式的類庫都是獨立的,保證相互隔離
  2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫載入進虛擬機器,必然會帶來記憶體消耗過高的問題。
  3. web容器也有自己依賴的類庫,不能與應用程式的類庫混淆。基於安全考慮,應該讓容器的類庫和程式的類庫隔離開

Tomcat類載入機制

認識Tomcat中的類載入器- whvixd | blog

  • Common類載入器作為 Catalina類載入器Shared類載入器 的父載入器。Common類載入器 能載入的類都可以被 Catalina類載入器Shared類載入器 使用。因此,Common類載入器 是為了實現公共類庫(可以被所有 Web 應用和 Tomcat 內部元件使用的類庫)的共享和隔離。

  • Catalina類載入器Shared類載入器 能載入的類則與對方相互隔離。Catalina類載入器 用於載入 Tomcat 自身的類,為了隔離 Tomcat 本身的類和 Web 應用的類。

  • Shared類載入器 作為 WebApp類載入器 的父載入器,專門來載入 Web 應用之間共享的類比如 Spring、Mybatis。

  • 每個 Web 應用都會建立一個單獨的 WebApp類載入器,並在啟動 Web 應用的執行緒裡設定執行緒執行緒上下文類載入器為 WebAppClassLoader,各個 WebAppClassLoader 例項之間相互隔離,進而實現 Web 應用之間的類隔。

Tomcat如何破壞雙親委派?

假設 Tomcat 伺服器載入了一個 Spring Jar 包。專案中用到的基礎類庫,由於其是 Web 應用之間共享的,因此會由 SharedClassLoader 載入。專案中一些用到了 Spring 的業務類,比如實現了 Spring 提供的介面、用到了 Spring 提供的註解。所以,載入 Spring 的類載入器(也就是 SharedClassLoader)也會用來載入這些業務類。但是業務類在 Web 應用目錄下,不在 SharedClassLoader 的載入路徑下,所以 SharedClassLoader 無法找到業務類,也就無法載入它們。

如何解決這個問題呢?

這個時候就需要用到 執行緒上下文類載入器(ThreadContextClassLoader

當 Spring 需要載入業務類的時候,它不是用自己的類載入器,而是用當前執行緒的上下文類載入器。還記得我上面說的嗎?每個 Web 應用都會建立一個單獨的 WebAppClassLoader,並在啟動 Web 應用的執行緒裡設定執行緒執行緒上下文類載入器為 WebAppClassLoader。這樣就可以讓高層的類載入器(SharedClassLoader)藉助子類載入器( WebAppClassLoader)來載入業務類,從而破壞了 Java 類載入的雙親委託機制。


參考內容

十分鐘搞懂Java類載入

Java 類載入機制

類載入器詳解(重點)

類載入器

請你簡單說一下類載入機制的實現原理?

相關文章