攜程一面:什么是雙親委派模型?
點擊關(guān)注公眾號,Java干貨及時送達

就算是不準(zhǔn)備面試,學(xué)習(xí)雙親委派模型對于我們也非常有幫助。我們比較熟悉的 Tomcat 服務(wù)器為了實現(xiàn) Web 應(yīng)用的隔離,就自定義了類加載并打破了雙親委派模型。
這篇文章我會先介紹類加載器,再介紹雙親委派模型,這樣有助于我們更好地理解。
目錄概覽:

回顧一下類加載過程
開始介紹類加載器和雙親委派模型之前,簡單回顧一下類加載過程。
類加載過程:加載->連接->初始化。 連接過程又可分為三步:驗證->準(zhǔn)備->解析。

加載是類加載過程的第一步,主要完成下面 3 件事情:
通過全類名獲取定義此類的二進制字節(jié)流 將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu) 在內(nèi)存中生成一個代表該類的 Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口
類加載器
類加載器介紹
類加載器從 JDK 1.0 就出現(xiàn)了,最初只是為了滿足 Java Applet(已經(jīng)被淘汰) 的需要。后來,慢慢成為 Java 程序中的一個重要組成部分,賦予了 Java 類可以被動態(tài)加載到 JVM 中并執(zhí)行的能力。
根據(jù)官方 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.
翻譯過來大概的意思是:
類加載器是一個負(fù)責(zé)加載類的對象。
ClassLoader是一個抽象類。給定類的二進制名稱,類加載器應(yīng)嘗試定位或生成構(gòu)成類定義的數(shù)據(jù)。典型的策略是將名稱轉(zhuǎn)換為文件名,然后從文件系統(tǒng)中讀取該名稱的“類文件”。每個 Java 類都有一個引用指向加載它的
ClassLoader。不過,數(shù)組類不是通過ClassLoader創(chuàng)建的,而是通過 JVM 在需要的時候自動創(chuàng)建的,數(shù)組類通過getClassLoader()方法獲取ClassLoader的時候和該數(shù)組的元素類型的ClassLoader是一致的。
從上面的介紹可以看出:
類加載器是一個負(fù)責(zé)加載類的對象,用于實現(xiàn)類加載過程中的加載這一步。 每個 Java 類都有一個引用指向加載它的 ClassLoader。數(shù)組類不是通過 ClassLoader創(chuàng)建的(數(shù)組類沒有對應(yīng)的二進制字節(jié)流),是由 JVM 直接生成的。
class Class<T> {
...
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {
//...
}
...
}
簡單來說,類加載器的主要作用就是加載 Java 類的字節(jié)碼(.class文件)到 JVM 中(在內(nèi)存中生成一個代表該類的Class對象)。字節(jié)碼可以是 Java 源程序(.java文件)經(jīng)過javac編譯得來,也可以是通過工具動態(tài)生成或者通過網(wǎng)絡(luò)下載得來。
其實除了加載類之外,類加載器還可以加載 Java 應(yīng)用所需的資源如文本、圖像、配置文件、視頻等等文件資源。本文只討論其核心功能:加載類。
類加載器加載規(guī)則
JVM 啟動的時候,并不會一次性加載所有的類,而是根據(jù)需要去動態(tài)加載。也就是說,大部分類在具體用到的時候才會去加載,這樣對內(nèi)存更加友好。
對于已經(jīng)加載的類會被放在ClassLoader中。在類加載的時候,系統(tǒng)會首先判斷當(dāng)前類是否被加載過。已經(jīng)被加載的類會直接返回,否則才會嘗試加載。也就是說,對于一個類加載器來說,相同二進制名稱的類只會被加載一次。
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由這個類加載器加載的類。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM調(diào)用,用此類加載器記錄每個已加載類。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}
類加載器總結(jié)
JVM 中內(nèi)置了三個重要的ClassLoader:
BootstrapClassLoader(啟動類加載器):最頂層的加載類,由 C++實現(xiàn),通常表示為 null,并且沒有父級,主要用來加載 JDK 內(nèi)部的核心類庫(%JAVA_HOME%/lib目錄下的rt.jar、resources.jar、charsets.jar等 jar 包和類)以及被-Xbootclasspath參數(shù)指定的路徑下的所有類。ExtensionClassLoader(擴展類加載器):主要負(fù)責(zé)加載%JRE_HOME%/lib/ext目錄下的 jar 包和類以及被java.ext.dirs系統(tǒng)變量所指定的路徑下的所有類。AppClassLoader(應(yīng)用程序類加載器):面向我們用戶的加載器,負(fù)責(zé)加載當(dāng)前應(yīng)用 classpath 下的所有 jar 包和類。
?? 拓展一下:
rt.jar:rt 代表“RunTime”,rt.jar是Java基礎(chǔ)類庫,包含Java doc里面看到的所有的類的類文件。也就是說,我們常用內(nèi)置庫java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。Java 9 引入了模塊系統(tǒng),并且略微更改了上述的類加載器。擴展類加載器被改名為平臺類加載器(platform class loader)。Java SE 中除了少數(shù)幾個關(guān)鍵模塊,比如說 java.base是由啟動類加載器加載之外,其他的模塊均由平臺類加載器所加載。
除了這三種類加載器之外,用戶還可以加入自定義的類加載器來進行拓展,以滿足自己的特殊需求。就比如說,我們可以對 Java 類的字節(jié)碼(.class文件)進行加密,加載時再利用自定義的類加載器對其解密。

除了BootstrapClassLoader是 JVM 自身的一部分之外,其他所有的類加載器都是在 JVM 外部實現(xiàn)的,并且全都繼承自ClassLoader抽象類。這樣做的好處是用戶可以自定義類加載器,以便讓應(yīng)用程序自己決定如何去獲取所需的類。
每個ClassLoader可以通過getParent()獲取其父ClassLoader,如果獲取到ClassLoader為null的話,那么該類是通過BootstrapClassLoader加載的。
public abstract class ClassLoader {
...
// 父加載器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
...
}
為什么 獲取到ClassLoader為null就是BootstrapClassLoader加載的呢?這是因為BootstrapClassLoader由 C++ 實現(xiàn),由于這個 C++ 實現(xiàn)的類加載器在 Java 中是沒有與之對應(yīng)的類的,所以拿到的結(jié)果是 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");
}
}
}
}
輸出結(jié)果(JDK 8 ):
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
從輸出結(jié)果可以看出:
我們編寫的 Java 類 PrintClassLoaderTree的ClassLoader是AppClassLoader;AppClassLoader的父ClassLoader是ExtClassLoader;ExtClassLoader的父ClassLoader是Bootstrap ClassLoader,因此輸出結(jié)果為 null。
自定義類加載器
我們前面也說說了,除了BootstrapClassLoader其他類加載器均由 Java 實現(xiàn)且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承ClassLoader抽象類。
ClassLoader類有兩個關(guān)鍵的方法:
protected Class loadClass(String name, boolean resolve):加載指定二進制名稱的類,實現(xiàn)了雙親委派機制 。name為類的二進制名稱,resove如果為 true,在加載時調(diào)用resolveClass(Class<?> c)方法解析該類。protected Class findClass(String name):根據(jù)類的二進制名稱來查找類,默認(rèn)實現(xiàn)是空方法。
官方 API 文檔中寫到:
Subclasses of
ClassLoaderare encouraged to overridefindClass(String name), rather than this method.建議
ClassLoader的子類重寫findClass(String name)方法而不是loadClass(String name, boolean resolve)方法。
如果我們不想打破雙親委派模型,就重寫ClassLoader類中的findClass()方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫loadClass()方法。
雙親委派模型
雙親委派模型介紹
類加載器有很多種,當(dāng)我們想要加載一個類的時候,具體是哪個類加載器加載呢?這就需要提到雙親委派模型了。
根據(jù)官網(wǎng)介紹:
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實例都有一個相關(guān)的父類加載器。需要查找類或資源時,ClassLoader實例會在試圖親自查找類或資源之前,將搜索類或資源的任務(wù)委托給其父類加載器。虛擬機中被稱為 "bootstrap class loader"的內(nèi)置類加載器本身沒有父類加載器,但是可以作為ClassLoader實例的父類加載器。
從上面的介紹可以看出:
ClassLoader類使用委托模型來搜索類和資源。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)有自己的父類加載器。 ClassLoader實例會在試圖親自查找類或資源之前,將搜索類或資源的任務(wù)委托給其父類加載器。
下圖展示的各種類加載器之間的層次關(guān)系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。

注意??:雙親委派模型并不是一種強制性的約束,只是 JDK 官方推薦的一種方式。如果我們因為某些特殊需求想要打破雙親委派模型,也是可以的,后文會介紹具體的方法。
其實這個雙親翻譯的容易讓別人誤解,我們一般理解的雙親都是父母,這里的雙親更多地表達的是“父母這一輩”的人而已,并不是說真的有一個MotherClassLoader和一個FatherClassLoader。個人覺得翻譯成單親委派模型更好一些,不過,國內(nèi)既然翻譯成了雙親委派模型并流傳了,按照這個來也沒問題,不要被誤解了就好。
另外,類加載器之間的父子關(guān)系一般不是以繼承的關(guān)系來實現(xiàn)的,而是通常使用組合關(guān)系來復(fù)用父加載器的代碼。
public abstract class ClassLoader {
...
// 組合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...
}
在面向?qū)ο缶幊讨校幸粭l非常經(jīng)典的設(shè)計原則:組合優(yōu)于繼承,多用組合少用繼承。
雙親委派模型的執(zhí)行流程
雙親委派模型的實現(xiàn)代碼非常簡單,邏輯非常清晰,都集中在java.lang.ClassLoader的loadClass()中,相關(guān)代碼如下所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,檢查該類是否已經(jīng)加載過
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 為 null,則說明該類沒有被加載過
long t0 = System.nanoTime();
try {
if (parent != null) {
//當(dāng)父類的加載器不為空,則通過父類的loadClass來加載該類
c = parent.loadClass(name, false);
} else {
//當(dāng)父類的加載器為空,則調(diào)用啟動類加載器來加載該類
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父類的類加載器無法找到相應(yīng)的類,則拋出異常
}
if (c == null) {
//當(dāng)父類加載器無法加載時,則調(diào)用findClass方法來加載該類
//用戶可通過覆寫該方法,來自定義類加載器
long t1 = System.nanoTime();
c = findClass(name);
//用于統(tǒng)計類加載器相關(guān)的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//對類進行l(wèi)ink操作
resolveClass(c);
}
return c;
}
}
每當(dāng)一個類加載器接收到加載請求時,它會先將請求轉(zhuǎn)發(fā)給父類加載器。在父類加載器沒有找到所請求的類的情況下,該類加載器才會嘗試去加載。
結(jié)合上面的源碼,簡單總結(jié)一下雙親委派模型的執(zhí)行流程:
在類加載的時候,系統(tǒng)會首先判斷當(dāng)前類是否被加載過。已經(jīng)被加載的類會直接返回,否則才會嘗試加載(每個父類加載器都會走一遍這個流程)。 類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調(diào)用父加載器 loadClass()方法來加載類)。這樣的話,所有的請求最終都會傳送到頂層的啟動類加載器BootstrapClassLoader中。只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載(調(diào)用自己的 findClass()方法來加載類)。
?? 拓展一下:
JVM 判定兩個 Java 類是否相同的具體規(guī)則:JVM 不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認(rèn)為兩個類是相同的。即使兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相同。
雙親委派模型的好處
雙親委派模型保證了 Java 程序的穩(wěn)定運行,可以避免類的重復(fù)加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。
如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現(xiàn)一些問題,比如我們編寫一個稱為java.lang.Object類的話,那么程序運行的時候,系統(tǒng)就會出現(xiàn)兩個不同的Object類。雙親委派模型可以保證加載的是 JRE 里的那個Object類,而不是你寫的Object類。這是因為AppClassLoader在加載你的Object類時,會委托給ExtClassLoader去加載,而ExtClassLoader又會委托給BootstrapClassLoader,BootstrapClassLoader發(fā)現(xiàn)自己已經(jīng)加載過了Object類,會直接返回,不會去加載你寫的Object類。
打破雙親委派模型方法
自定義加載器的話,需要繼承ClassLoader。如果我們不想打破雙親委派模型,就重寫ClassLoader類中的findClass()方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫loadClass()方法。
為什么是重寫loadClass()方法打破雙親委派模型呢?雙親委派模型的執(zhí)行流程已經(jīng)解釋了:
類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調(diào)用父加載器
loadClass()方法來加載類)。
我們比較熟悉的 Tomcat 服務(wù)器為了能夠優(yōu)先加載 Web 應(yīng)用目錄下的類,然后再加載其他目錄下的類,就自定義了類加載器WebAppClassLoader來打破雙親委托機制。這也是 Tomcat 下 Web 應(yīng)用之間的類實現(xiàn)隔離的具體原理。
Tomcat 的類加載器的層次結(jié)構(gòu)如下:

感興趣的小伙伴可以自行研究一下 Tomcat 類加載器的層次結(jié)構(gòu),這有助于我們搞懂 Tomcat 隔離 Web 應(yīng)用的原理,推薦資料是《深入拆解 Tomcat & Jetty》。
推薦閱讀
《深入拆解 Java 虛擬機》 深入分析 Java ClassLoader 原理:https://blog.csdn.net/xyang81/article/details/7292380 Java 類加載器(ClassLoader):http://gityuan.com/2016/01/24/java-classloader/ Class Loaders in Java:https://www.baeldung.com/java-classloaders Class ClassLoader - Oracle 官方文檔:https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html 老大難的 Java ClassLoader 再不理解就老了:https://zhuanlan.zhihu.com/p/51374915
往 期 推 薦
3、互聯(lián)網(wǎng)人為什么學(xué)不會擺爛
4、為什么國外JetBrains做 IDE 就可以養(yǎng)活自己,國內(nèi)不行?區(qū)別在哪?
點分享
點收藏
點點贊
點在看





