淺析雙親委派機制

爱为斯坦發表於2024-10-29

雙親委派機制

1)什麼是雙親委派

虛擬機器在載入類的過程中需要使用類載入器進行載入,而在Java中,類載入器有很多,那麼當JVM想要載入一個.class檔案的時候,到底應該由哪個類載入器載入呢?這就不得不提到"雙親委派機制"。

首先,我們需要知道的是,Java語言系統中支援以下4種類載入器:

  • Bootstrap ClassLoader 啟動類載入器:主要負責載入Java核心類庫,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader 標準擴充套件類載入器:主要負責載入目錄%JRE_HOME%\lib\ext目錄下的jar包和class檔案。
  • Application ClassLoader 應用類載入器:主要負責載入當前應用的classpath下的所有類。
  • User ClassLoader 使用者自定義類載入器:使用者自定義的類載入器,可載入指定路徑的class檔案。

也就是說,一個使用者自定義的類,如com.hollis.ClassHollis 是無論如何也不會被Bootstrap和Extention載入器載入的

這四種類載入器之間,是存在著一種層次關係的,如下圖

一般認為上一層載入器是下一層載入器的父載入器,那麼,除了BootstrapClassLoader之外,所有的載入器都是有父載入器的。

那麼,所謂的雙親委派機制,指的就是:當一個類載入器收到了類載入的請求的時候,他不會直接去載入指定的類,而是把這個請求委託給自己的父載入器去載入。只有父載入器無法載入這個類的時候,才會由當前這個載入器來負責類的載入。

2)為什麼需要雙親委派

因為類載入器之間有嚴格的層次關係,那麼也就使得Java類也隨之具備了層次關係。比如一個定義在java.lang包下的類,因為它被存放在rt.jar之中,所以在被載入過程彙總,會被一直委託到Bootstrap ClassLoader,最終由Bootstrap ClassLoader所載入。

而一個使用者自定義的com.hollis.ClassHollis類,他也會被一直委託到Bootstrap ClassLoader,但是因為Bootstrap ClassLoader不負責載入該類,那麼會在由Extention ClassLoader嘗試載入,而Extention ClassLoader也不負責這個類的載入,最終才會被Application ClassLoader載入。

這種機制有幾個好處。

首先,透過委派的方式,可以避免類的重複載入,當父載入器已經載入過某一個類時,子載入器就不會再重新載入這個類。

另外,透過雙親委派的方式,還保證了安全性。假如我們自己編寫一個類java.util.Object,它的實現可能有一定的危險性或者隱藏的bug。而我們知道Java自帶的核心類裡面也有java.util.Object,如果JVM啟動的時候先行載入的是我們自己編寫的java.util.Object,那麼就有可能出現安全問題!

3)載入器之間的關係

雙親委派模型中,類載入器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父載入器的程式碼的。

如下為ClassLoader中父載入器的定義:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;
}

4)雙親委派的實現原理

雙親委派模型對於保證Java程式的穩定運作很重要,但它的實現並不複雜。

實現雙親委派的程式碼都集中在java.lang.ClassLoader的loadClass()方法之中

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();
                    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;
        }
    }

程式碼不難理解,主要就是以下幾個步驟:

1、先檢查類是否已經被載入過

2、若沒有載入則呼叫父載入器的loadClass()方法進行載入

3、若父載入器為空則預設使用啟動類載入器作為父載入器。

4、如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。

5)為什麼需要破壞雙親委派

假設我們有一個介面 PaymentService 和兩個實現類:

  • PaymentServiceImplA(來自 ThirdPartyA
  • PaymentServiceImplB(來自 ThirdPartyB

如果這兩個實現類都被放在應用的類路徑下,並且你使用了雙親委派機制,載入過程如下:

  1. 載入請求:當你在程式碼中使用 PaymentServiceImplAPaymentServiceImplB 時,類載入器會收到載入請求。
  2. 委派機制
    • 首先,Bootstrap ClassLoader 會嘗試載入請求的類。如果該類不在 Java 核心庫中(比如 java.langjava.util 等),則 Bootstrap ClassLoader 會失敗。
    • 然後,Extension ClassLoader 會嘗試載入。如果類仍然不在擴充套件庫中,則載入失敗。
    • 接下來,Application ClassLoader(也稱為 Web ClassLoader,在 Web 應用中)會嘗試載入這個類。
  3. 載入的結果
    • 如果 PaymentServiceImplAPaymentServiceImplB 都存在於類路徑下,Application ClassLoader 將會載入它們。但因為它們屬於不同的第三方庫,它們的類名必須是唯一的。

可能出現的問題

  • 命名衝突:如果 ThirdPartyAThirdPartyB 的實現類都定義為 PaymentServiceImpl,這將導致類命名衝突,最終只有一個實現會被載入,而另一個可能會被忽略或引發錯誤。
  • 版本衝突:如果 PaymentServiceImplAPaymentServiceImplB 依賴於不同版本的同一庫(例如,commons-logging),它們將會載入到同一個 Application ClassLoader 中,這也可能導致執行時錯誤。

6)怎麼破壞雙親委派

知道了雙親委派模型的實現,那麼想要破壞雙親委派機制就很簡單了。

因為他的雙親委派過程都是在loadClass方法中實現的,那麼想要破壞這種機制,那麼就自定義一個類載入器,重寫其中的loadClass方法,使其不進行雙親委派即可。

loadClass()、findClass()、defineClass()區別

ClassLoader中和類載入有關的方法有很多,前面提到了loadClass,除此之外,還有findClass和defineClass等,那麼這幾個方法有什麼區別呢?

  • loadClass()
    • 就是主要進行類載入的方法,預設的雙親委派機制就實現在這個方法中。
  • findClass()
    • 根據名稱或位置載入.class位元組碼
  • definclass()
    • 把位元組碼轉化為Class

這裡面需要展開講一下loadClass和findClass,我們前面說過,當我們想要自定義一個類載入器的時候,並且像破壞雙親委派原則時,我們會重寫loadClass方法。

那麼,如果我們想定義一個類載入器,但是不想破壞雙親委派模型的時候呢?

這時候,就可以繼承ClassLoader,並且重寫findClass方法。findClass()方法是JDK1.2之後的ClassLoader新新增的一個方法。

 /**
 * @since  1.2
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

這個方法只丟擲了一個異常,沒有預設實現。

JDK1.2之後已不再提倡使用者直接覆蓋loadClass()方法,而是建議把自己的類載入邏輯實現到findClass()方法中。

因為在loadClass()方法的邏輯裡,如果父類載入器載入失敗,則會呼叫自己的findClass()方法來完成載入。

所以,如果你想定義一個自己的類載入器,並且要遵守雙親委派模型,那麼可以繼承ClassLoader,並且在findClass中實現你自己的載入邏輯即可。

7)破壞雙親委派的例子

1. Tomcat 的類載入機制

背景

Tomcat 是一個廣泛使用的 Java Web 伺服器和 Servlet 容器。它使用自己的類載入機制來處理 Web 應用和其他元件的類載入。

破壞雙親委派的原因

  • 隔離性:Tomcat 需要將不同的 Web 應用(WAR 檔案)隔離開來,以防止它們之間的類衝突。不同應用可能會依賴於相同名稱的類,但這些類的實現可能不同。
  • 靈活性:允許不同的 Web 應用使用不同版本的同一個庫(例如,commons-logging),而不影響其他應用。

實現

Tomcat 使用多個類載入器:

  • Web 應用類載入器:負責載入應用的類。
  • 父載入器(通常是 Application ClassLoader):負責載入 Tomcat 自身的類和一些共享庫。

具體而言,當 Tomcat 載入一個 Web 應用時,它的類載入器會優先載入應用內的類。如果該類在應用內找不到,才會委託給父載入器。這種方式允許 Web 應用使用自己的類而不是全域性共享的類。

2. JBoss/WildFly 的類載入機制

背景

JBoss/WildFly 是另一個流行的 Java EE 應用伺服器,採用了類似的策略來處理類載入。

破壞雙親委派的原因

  • 模組化:JBoss/WildFly 允許開發者將應用劃分為多個模組,每個模組可以有自己獨立的依賴。
  • 防止版本衝突:允許不同模組之間使用不同版本的相同庫。

實現

  • JBoss/WildFly 使用模組載入器,模組中的類預設不經過雙親委派機制。每個模組都可以有自己的類路徑,減少類衝突的風險。
3. 例子總結

例如,如果在 Tomcat 中有兩個 Web 應用:

  • 應用 A 使用 commons-logging 的 1.1 版本。
  • 應用 B 使用 commons-logging 的 1.2 版本。

如果不破壞雙親委派機制,兩個應用會共享同一個 commons-logging 類,這可能導致執行時錯誤和版本不相容。但由於 Tomcat 的類載入器會優先載入應用自身的類,因此各自的 commons-logging 版本會被正確載入。

8)類載入器的使用場景

什麼時候使用預設載入器
  1. 常規應用開發
    • 大多數 Java 應用程式、Web 應用和企業應用都可以使用預設的類載入器(Application ClassLoader),因為它能夠從類路徑中自動載入需要的類。
  2. 使用標準庫
    • 當你的應用僅依賴於 Java 標準庫和已經在類路徑下的第三方庫時,預設類載入器通常足夠。
什麼時候使用自定義類載入器
  1. 特殊路徑載入
    • 當需要從非標準路徑(如網路、資料庫或特定資料夾)載入類時,使用自定義類載入器可以滿足這種需求。
  2. 版本衝突管理
    • 在同一專案中需要使用多個版本的同名類時,自定義類載入器可以隔離它們,避免命名衝突。
  3. 動態生成類
    • 如果你的應用需要在執行時動態生成和載入類(例如,透過位元組碼操作庫),則自定義類載入器是必要的。
  4. 類增強和修改
    • 對於需要在類載入時進行位元組碼修改或增強(如 AOP 框架),使用自定義類載入器是合適的。

參考:https://www.cnblogs.com/hollischuang/p/14260801.html

相關文章