天天看點

攜程一面:什麼是雙親委派模型?

作者:java小悠

這是攜程一面的一個 JVM 面試真題。參加過校招面試的同學,應該對這個問題不陌生。一般提問 JVM 知識點的時候,就會順帶問你雙親委派模型(别扭的翻譯。。。)。

攜程一面:什麼是雙親委派模型?

就算是不準備面試,學習雙親委派模型對于我們也非常有幫助。我們比較熟悉的 Tomcat 伺服器為了實作 Web 應用的隔離,就自定義了類加載并打破了雙親委派模型。

這篇文章我會先介紹類加載器,再介紹雙親委派模型,這樣有助于我們更好地了解。

目錄概覽:

攜程一面:什麼是雙親委派模型?

回顧一下類加載過程

開始介紹類加載器和雙親委派模型之前,簡單回顧一下類加載過程。

  • 類加載過程:加載->連接配接->初始化。
  • 連接配接過程又可分為三步:驗證->準備->解析。
攜程一面:什麼是雙親委派模型?

類加載過程

加載是類加載過程的第一步,主要完成下面 3 件事情:

  1. 通過全類名擷取定義此類的二進制位元組流
  2. 将位元組流所代表的靜态存儲結構轉換為方法區的運作時資料結構
  3. 在記憶體中生成一個代表該類的Class對象,作為方法區這些資料的通路入口

類加載器

類加載器介紹

類加載器從 JDK 1.0 就出現了,最初隻是為了滿足 Java Applet(已經被淘汰) 的需要。後來,慢慢成為 Java 程式中的一個重要組成部分,賦予了 Java 類可以被動态加載到 JVM 中并執行的能力。

根據官方 API 文檔的介紹:

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.

Every Class object contains a reference to the ClassLoader that defined it.

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

翻譯過來大概的意思是:

類加載器是一個負責加載類的對象。ClassLoader是一個抽象類。給定類的二進制名稱,類加載器應嘗試定位或生成構成類定義的資料。典型的政策是将名稱轉換為檔案名,然後從檔案系統中讀取該名稱的“類檔案”。

每個 Java 類都有一個引用指向加載它的ClassLoader。不過,數組類不是通過ClassLoader建立的,而是通過 JVM 在需要的時候自動建立的,數組類通過getClassLoader()方法擷取ClassLoader的時候和該數組的元素類型的ClassLoader是一緻的。

從上面的介紹可以看出:

  • 類加載器是一個負責加載類的對象,用于實作類加載過程中的加載這一步。
  • 每個 Java 類都有一個引用指向加載它的ClassLoader。
  • 數組類不是通過ClassLoader建立的(數組類沒有對應的二進制位元組流),是由 JVM 直接生成的。
class Class<T> {
  ...
  private final ClassLoader classLoader;
  @CallerSensitive
  public ClassLoader getClassLoader() {
     //...
  }
  ...
}
           

簡單來說,類加載器的主要作用就是加載 Java 類的位元組碼(.class檔案)到 JVM 中(在記憶體中生成一個代表該類的Class對象)。位元組碼可以是 Java 源程式(.java檔案)經過javac編譯得來,也可以是通過工具動态生成或者通過網絡下載下傳得來。

其實除了加載類之外,類加載器還可以加載 Java 應用所需的資源如文本、圖像、配置檔案、視訊等等檔案資源。本文隻讨論其核心功能:加載類。

類加載器加載規則

JVM 啟動的時候,并不會一次性加載所有的類,而是根據需要去動态加載。也就是說,大部分類在具體用到的時候才會去加載,這樣對記憶體更加友好。

對于已經加載的類會被放在ClassLoader中。在類加載的時候,系統會首先判斷目前類是否被加載過。已經被加載的類會直接傳回,否則才會嘗試加載。也就是說,對于一個類加載器來說,相同二進制名稱的類隻會被加載一次。

public abstract class ClassLoader {
  ...
  private final ClassLoader parent;
  // 由這個類加載器加載的類。
  private final Vector<Class<?>> classes = new Vector<>();
  // 由VM調用,用此類加載器記錄每個已加載類。
  void addClass(Class<?> c) {
        classes.addElement(c);
   }
  ...
}
           

類加載器總結

JVM 中内置了三個重要的ClassLoader:

  1. BootstrapClassLoader(啟動類加載器):最頂層的加載類,由 C++實作,通常表示為 null,并且沒有父級,主要用來加載 JDK 内部的核心類庫(%JAVA_HOME%/lib目錄下的rt.jar、resources.jar、charsets.jar等 jar 包和類)以及被-Xbootclasspath參數指定的路徑下的所有類。
  2. ExtensionClassLoader(擴充類加載器):主要負責加載%JRE_HOME%/lib/ext目錄下的 jar 包和類以及被java.ext.dirs系統變量所指定的路徑下的所有類。
  3. AppClassLoader(應用程式類加載器):面向我們使用者的加載器,負責加載目前應用 classpath 下的所有 jar 包和類。

拓展一下:

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是由啟動類加載器加載之外,其他的子產品均由平台類加載器所加載。

除了這三種類加載器之外,使用者還可以加入自定義的類加載器來進行拓展,以滿足自己的特殊需求。就比如說,我們可以對 Java 類的位元組碼(.class檔案)進行加密,加載時再利用自定義的類加載器對其解密。

攜程一面:什麼是雙親委派模型?

類加載器層次關系圖

除了BootstrapClassLoader是 JVM 自身的一部分之外,其他所有的類加載器都是在 JVM 外部實作的,并且全都繼承自ClassLoader抽象類。這樣做的好處是使用者可以自定義類加載器,以便讓應用程式自己決定如何去擷取所需的類。

每個ClassLoader可以通過getParent()擷取其父ClassLoader,如果擷取到ClassLoader為null的話,那麼該類是通過BootstrapClassLoader加載的。

public abstract class ClassLoader {
  ...
  // 父加載器
  private final ClassLoader parent;
  @CallerSensitive
  public final ClassLoader getParent() {
     //...
  }
  ...
}
           

為什麼 擷取到ClassLoader為null就是BootstrapClassLoader加載的呢?這是因為BootstrapClassLoader由 C++ 實作,由于這個 C++ 實作的類加載器在 Java 中是沒有與之對應的類的,是以拿到的結果是 null。

下面我們來看一個擷取ClassLoader的小案例:

public class PrintClassLoaderTree {

    public static void main(String[] args) {

        ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();

        StringBuilder split = new StringBuilder("|--");
        boolean needContinue = true;
        while (needContinue){
            System.out.println(split.toString() + classLoader);
            if(classLoader == null){
                needContinue = false;
            }else{
                classLoader = classLoader.getParent();
                split.insert(0, "\t");
            }
        }
    }

}
           

輸出結果(JDK 8 ):

|--sun.misc.Launcher$AppClassLoader@18b4aac2
    |--sun.misc.Launcher$ExtClassLoader@53bd815b
        |--null
           

從輸出結果可以看出:

  • 我們編寫的 Java 類PrintClassLoaderTree的ClassLoader是AppClassLoader;
  • AppClassLoader的父ClassLoader是ExtClassLoader;
  • ExtClassLoader的父ClassLoader是Bootstrap ClassLoader,是以輸出結果為 null。

自定義類加載器

我們前面也說說了,除了BootstrapClassLoader其他類加載器均由 Java 實作且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承ClassLoader抽象類。

ClassLoader類有兩個關鍵的方法:

  • protected Class loadClass(String name, boolean resolve):加載指定二進制名稱的類,實作了雙親委派機制 。name為類的二進制名稱,resove如果為 true,在加載時調用resolveClass(Class<?> c)方法解析該類。
  • protected Class findClass(String name):根據類的二進制名稱來查找類,預設實作是空方法。

官方 API 文檔中寫到:

Subclasses ofClassLoaderare encouraged to overridefindClass(String name), rather than this method.

建議ClassLoader的子類重寫findClass(String name)方法而不是loadClass(String name, boolean resolve)方法。

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

雙親委派模型

雙親委派模型介紹

類加載器有很多種,當我們想要加載一個類的時候,具體是哪個類加載器加載呢?這就需要提到雙親委派模型了。

根據官網介紹:

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.

翻譯過來大概的意思是:

ClassLoader類使用委托模型來搜尋類和資源。每個ClassLoader執行個體都有一個相關的父類加載器。需要查找類或資源時,ClassLoader執行個體會在試圖親自查找類或資源之前,将搜尋類或資源的任務委托給其父類加載器。虛拟機中被稱為 "bootstrap class loader"的内置類加載器本身沒有父類加載器,但是可以作為ClassLoader執行個體的父類加載器。

從上面的介紹可以看出:

  • ClassLoader類使用委托模型來搜尋類和資源。
  • 雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應有自己的父類加載器。
  • ClassLoader執行個體會在試圖親自查找類或資源之前,将搜尋類或資源的任務委托給其父類加載器。

下圖展示的各種類加載器之間的層次關系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。

攜程一面:什麼是雙親委派模型?

類加載器層次關系圖

注意⚠️:雙親委派模型并不是一種強制性的限制,隻是 JDK 官方推薦的一種方式。如果我們因為某些特殊需求想要打破雙親委派模型,也是可以的,後文會介紹具體的方法。

其實這個雙親翻譯的容易讓别人誤解,我們一般了解的雙親都是父母,這裡的雙親更多地表達的是“父母這一輩”的人而已,并不是說真的有一個MotherClassLoader和一個FatherClassLoader。個人覺得翻譯成單親委派模型更好一些,不過,國内既然翻譯成了雙親委派模型并流傳了,按照這個來也沒問題,不要被誤解了就好。

另外,類加載器之間的父子關系一般不是以繼承的關系來實作的,而是通常使用組合關系來複用父加載器的代碼。

public abstract class ClassLoader {
  ...
  // 組合
  private final ClassLoader parent;
  protected ClassLoader(ClassLoader parent) {
       this(checkCreateClassLoader(), parent);
  }
  ...
}
           

在面向對象程式設計中,有一條非常經典的設計原則:組合優于繼承,多用組合少用繼承。

雙親委派模型的執行流程

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

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

每當一個類加載器接收到加載請求時,它會先将請求轉發給父類加載器。在父類加載器沒有找到所請求的類的情況下,該類加載器才會嘗試去加載。

結合上面的源碼,簡單總結一下雙親委派模型的執行流程:

  • 在類加載的時候,系統會首先判斷目前類是否被加載過。已經被加載的類會直接傳回,否則才會嘗試加載(每個父類加載器都會走一遍這個流程)。
  • 類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調用父加載器loadClass()方法來加載類)。這樣的話,所有的請求最終都會傳送到頂層的啟動類加載器BootstrapClassLoader中。
  • 隻有當父加載器回報自己無法完成這個加載請求(它的搜尋範圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載(調用自己的findClass()方法來加載類)。

拓展一下:

JVM 判定兩個 Java 類是否相同的具體規則:JVM 不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。隻有兩者都相同的情況,才認為兩個類是相同的。即使兩個類來源于同一個Class檔案,被同一個虛拟機加載,隻要加載它們的類加載器不同,那這兩個類就必定不相同。

雙親委派模型的好處

雙親委派模型保證了 Java 程式的穩定運作,可以避免類的重複加載(JVM 區分不同類的方式不僅僅根據類名,相同的類檔案被不同的類加載器加載産生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。

如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱為java.lang.Object類的話,那麼程式運作的時候,系統就會出現兩個不同的Object類。雙親委派模型可以保證加載的是 JRE 裡的那個Object類,而不是你寫的Object類。這是因為AppClassLoader在加載你的Object類時,會委托給ExtClassLoader去加載,而ExtClassLoader又會委托給BootstrapClassLoader,BootstrapClassLoader發現自己已經加載過了Object類,會直接傳回,不會去加載你寫的Object類。

打破雙親委派模型方法

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

為什麼是重寫loadClass()方法打破雙親委派模型呢?雙親委派模型的執行流程已經解釋了:

類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調用父加載器loadClass()方法來加載類)。

我們比較熟悉的 Tomcat 伺服器為了能夠優先加載 Web 應用目錄下的類,然後再加載其他目錄下的類,就自定義了類加載器WebAppClassLoader來打破雙親委托機制。這也是 Tomcat 下 Web 應用之間的類實作隔離的具體原理。

Tomcat 的類加載器的層次結構如下:

攜程一面:什麼是雙親委派模型?

Tomcat 的類加載器的層次結構

感興趣的小夥伴可以自行研究一下 Tomcat 類加載器的層次結構,這有助于我們搞懂 Tomcat 隔離 Web 應用的原理,推薦資料是《深入拆解 Tomcat & Jetty》。

原文連結:https://mp.weixin.qq.com/s/gPCWrPZK29yb-zX_8b3CVA

繼續閱讀