本文淺析了雙親委派的基本概念、實(shí)現(xiàn)原理、和自定義類加載器的正確姿勢。
對于更細(xì)致的加載loading過程、初始化initialization順序等問題,文中暫不涉及,后面整理筆記時有相應(yīng)的文章。
JDK版本:oracle java 1.8.0_102
基本概念
定義
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。
雙親委派模型的工作過程是:
- 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
- 每一個層次的類加載器都是如此。因此,所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中。
- 只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。
很多人對“雙親”一詞很困惑。這是翻譯的鍋,,,“雙親”只是“parents”的直譯,實(shí)際上并不表示漢語中的父母雙親,而是一代一代很多parent,即parents。
作用
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨(dú)立的類名稱空間。因此,使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處:類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。
例如類java.lang.Object,它由啟動類加載器加載。雙親委派模型保證任何類加載器收到的對java.lang.Object的加載請求,最終都是委派給處于模型最頂端的啟動類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。
相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并用自定義的類加載器加載,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂。
結(jié)構(gòu)
系統(tǒng)提供的類加載器
在雙親委派模型的定義中提到了“啟動類加載器”。包括啟動類加載器,絕大部分Java程序都會使用到以下3種系統(tǒng)提供的類加載器:
- 啟動類加載器(Bootstrap ClassLoader)
負(fù)責(zé)將存放在<JAVA_HOME>/lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)按照文件名識別的(如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。
啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器,那直接使用null代替即可。
JDK中的常用類大都由啟動類加載器加載,如java.lang.String、java.util.List等。需要特別說明的是,啟動類Main class也由啟動類加載器加載。
- 擴(kuò)展類加載器(Extension ClassLoader)
由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。
負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。
開發(fā)者可以直接使用擴(kuò)展類加載器。
猴子對自己電腦
<JAVA_HOME>/lib/ext目錄下的jar包都非常陌生。看了幾個jar包,也沒找到常用的類;唯一有點(diǎn)印象的是jfxrt.jar,被用于JavaFX的開發(fā)之中。
- 應(yīng)用程序類加載器(Application ClassLoader)
由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個類加載器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。
它負(fù)責(zé)加載用戶類路徑ClassPath上所指定的類庫,開發(fā)者可以直接使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
啟動類Main class、其他如工程中編寫的類、maven引用的類,都會被放置在類路徑下。Main class由啟動類加載器加載,其他類由應(yīng)用程序類加載器加載。
自定義的類加載器
JVM建議用戶將應(yīng)用程序類加載器作為自定義類加載器的父類加載器。則類加載的雙親委派模型如圖:

實(shí)現(xiàn)原理
實(shí)現(xiàn)雙親委派的代碼都集中在ClassLoader#loadClass()方法之中。將統(tǒng)計(jì)部分的代碼去掉之后,簡寫如下:
public abstract class ClassLoader {
...
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
...
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
...
c = findClass(name);
// do some stats
...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
...
}
- 首先,檢查目標(biāo)類是否已在當(dāng)前類加載器的命名空間中加載(即,使用二元組
<類加載器實(shí)例,全限定名>區(qū)分不同類)。 - 如果沒有找到,則嘗試將請求委托給父類加載器(如果指定父類加載器為null,則將啟動類加載器作為父類加載器;如果沒有指定父類加載器,則將應(yīng)用程序類加載器作為父類加載器),最終所有類都會委托到啟動類加載器。
- 如果父類加載器加載失敗,則自己加載。
- 默認(rèn)resolve取false,不需要解析,直接返回。
自定義類加載器的正確姿勢
系統(tǒng)提供的3種類加載器分別負(fù)責(zé)各路徑下的Java類的加載。如果用戶希望自定義一個類加載器(如從網(wǎng)絡(luò)中讀取class字節(jié)流,以加載新的類),該如何做呢?
錯誤姿勢
先來看幾個類加載的錯誤姿勢。
再次提醒,以下這些錯誤姿勢一定不影響編譯,因?yàn)榧虞d行為發(fā)生在運(yùn)行期。
不定義類加載器
現(xiàn)在用戶自定義了一個sun.applet.Main類,但不定義類加載器:
package sun.applet;
/**
* Created by monkeysayhi on 2017/12/20.
*/
public class Main {
public Main() {
System.out.println("constructed");
}
public static void main(String[] args) {
System.out.println("recognized as sun.applet.Main in jdk," +
" and there isn't any main method");
}
}
為保持與后續(xù)實(shí)驗(yàn)的連貫性,這里沒有選擇常用的java.lang包下的類。原因見后。
將該類作為Main class啟動,會輸出什么呢?或許你以為會輸出12-13行聲明的字符串,現(xiàn)實(shí)卻總會啪啪啪撫摸我們的臉龐:
用法: appletviewer <options> url
其中, <options> 包括:
-debug 在 Java 調(diào)試器中啟動小應(yīng)用程序查看器
-encoding <encoding> 指定 HTML 文件使用的字符編碼
-J<runtime flag> 將參數(shù)傳遞到 java 解釋器
-J 選項(xiàng)是非標(biāo)準(zhǔn)選項(xiàng), 如有更改, 恕不另行通知。
不管這些東西從哪來的,總之不是我們定義的。
實(shí)際被選中的Main class是jdk中的sun.applet.Main類。如果沒有定義類加載器,則會使用默認(rèn)的類加載器(應(yīng)用程序類加載器)和默認(rèn)的類加載行為(ClassLoader#loadClass())。由雙親委派模型可知,最終將由啟動類加載器加載<JAVA_HOME>/lib/rt.jar中的sun.applet.Main,并執(zhí)行其main方法。
定義類加載器,但不委派
如何不委派呢?覆寫ClassLoader#loadClass():
當(dāng)然,還要覆寫ClassLoader#findClass()以支持自定義的類加載方式。
public class UnDelegationClassLoader extends ClassLoader {
private String classpath;
public UnDelegationClassLoader(String classpath) {
super(null);
this.classpath = classpath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clz = findLoadedClass(name);
if (clz != null) {
return clz;
}
// jdk 目前對"java."開頭的包增加了權(quán)限保護(hù),這些包我們?nèi)匀唤唤o jdk 加載
if (name.startsWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
MalformedURLException {
sun.applet.Main main1 = new sun.applet.Main();
UnDelegationClassLoader cl = new UnDelegationClassLoader("java-study/target/classes/");
String name = "sun.applet.Main";
Class<?> clz = cl.loadClass(name);
Object main2 = clz.newInstance();
System.out.println("main1 class: " + main1.getClass());
System.out.println("main2 class: " + main2.getClass());
System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
}
}
注意16-19行。由于jdk對"java."開頭的包增加了權(quán)限保護(hù),用戶無法使用示例中的ClassLoader#defineClass()方法;而所有類都是
java.lang.Object類的子類,sout輸出時也要使用java.lang.System類等,所以我們又必須加載java.lang包下的類。因此,我們?nèi)匀粚⑦@些包委托給jdk加載。同時,這也解釋了,為什么不能將常用的java.lang包下的類作為同名類測試對象。
示例先加載jdk中的sun.applet.Main類,實(shí)例化main1,再使用不進(jìn)行委派的自定義類加載器加載自定義的sun.applet.Main類,實(shí)例化main2。如果實(shí)例main2創(chuàng)建成功,則輸出“constructed”。之后,輸出main1、main2的類名和類加載器。
輸出:
constructed
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: com.msh.demo.classloading.loading.UnDelegationClassLoader@1d44bcfa
首先,1行說明實(shí)例main2創(chuàng)建成功了。2-3行表示main1、main2的全限定名確實(shí)相同。4-5行表示二者的類加載器不同,main1的類使用啟動類加載器,main2的類使用自定義的類加載器。
正確姿勢
一個符合規(guī)范的類加載器,應(yīng)當(dāng)僅覆寫ClassLoader#findClass(),以支持自定義的類加載方式。不建議覆寫ClassLoader#loadClass()(以使用默認(rèn)的類加載邏輯,即雙親委派模型);如果需要覆寫,則不應(yīng)該破壞雙親委派模型:
public class DelegationClassLoader extends ClassLoader {
private String classpath;
public DelegationClassLoader(String classpath, ClassLoader parent) {
super(parent);
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException,
MalformedURLException {
sun.applet.Main main1 = new sun.applet.Main();
DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/",
getSystemClassLoader());
String name = "sun.applet.Main";
Class<?> clz = cl.loadClass(name);
Object main2 = clz.newInstance();
System.out.println("main1 class: " + main1.getClass());
System.out.println("main2 class: " + main2.getClass());
System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
ClassLoader itrCl = cl;
while (itrCl != null) {
System.out.println(itrCl);
itrCl = itrCl.getParent();
}
}
}
因?yàn)樵谧远x類加載器上正確使用了雙親委派模型,上述代碼運(yùn)行后,不會出現(xiàn)相同全限定名的類被不同類加載器加載的問題,也就不會引起混亂了.
輸出:
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: null
com.msh.demo.classloading.loading.DelegationClassLoader@1d44bcfa
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@266474c2
在雙親委派模型下,運(yùn)行時中只存在啟動類加載器加載的sun.applet.Main類。
5-6行輸出了類加載器在雙親委派模型中的位置:最下層是自定義類加載器,然后逐層向上是應(yīng)用程序類加載器、擴(kuò)展類加載器,最上層是啟動類加載器(在擴(kuò)展類加載器中記為null)??膳c前面的結(jié)構(gòu)圖對照。
不過,實(shí)際情況中,覆寫ClassLoader#loadClass()是非常常見的。JNDI、OSGi等為了實(shí)現(xiàn)各自的需求,也在一定程度上破壞了雙親委派模型。
本文鏈接:
本文鏈接:淺談雙親委派模型
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。