面試官:不會(huì)有人不懂類加載器與雙親委派模型吧?
類加載器在加載階段,會(huì)將class文件加載進(jìn)方法區(qū)。有關(guān)類加載的全過(guò)程,可以先參考我的另外一篇文章類的奇幻漂流——類加載機(jī)制探秘
類加載器的類型
類加載器有以下種類:
- 啟動(dòng)類加載器(Bootstrap ClassLoader)
- 擴(kuò)展類加載器(Extension ClassLoader)
- 應(yīng)用類加載器(Application ClassLoader)
啟動(dòng)類加載器
內(nèi)嵌在JVM內(nèi)核中的加載器,由C++語(yǔ)言編寫(因此也不會(huì)繼承ClassLoader),是類加載器層次中最頂層的加載器。用于加載java的核心類庫(kù),即加載jre/lib/rt.jar里所有的class。由于啟動(dòng)類加載器涉及到虛擬機(jī)本地實(shí)現(xiàn)細(xì)節(jié),我們無(wú)法獲取啟動(dòng)類加載器的引用。
擴(kuò)展類加載器
它負(fù)責(zé)加載JRE的擴(kuò)展目錄,jre/lib/ext或者由java.ext.dirs系統(tǒng)屬性指定的目錄中jar包的類。父類加載器為啟動(dòng)類加載器,但使用擴(kuò)展類加載器調(diào)用getParent依然為null。
應(yīng)用類加載器
又稱系統(tǒng)類加載器,可用通過(guò) java.lang.ClassLoader.getSystemClassLoader()方法獲得此類加載器的實(shí)例,系統(tǒng)類加載器也因此得名。應(yīng)用類加載器主要加載classpath下的class,即用戶自己編寫的應(yīng)用編譯得來(lái)的class,調(diào)用getParent返回?cái)U(kuò)展類加載器。
擴(kuò)展類加載器與應(yīng)用類加載器繼承結(jié)構(gòu)如圖所示:

可以看到除了啟動(dòng)類加載器,其余的兩個(gè)類加載器都繼承于ClassLoader,我們自定義的類加載,也需要繼承ClassLoader。
雙親委派機(jī)制
當(dāng)一個(gè)類加載器收到了一個(gè)類加載請(qǐng)求時(shí),它自己不會(huì)先去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求轉(zhuǎn)交給父類加載器,每一個(gè)層的類加載器都是如此,因此所有的類加載請(qǐng)求都應(yīng)該傳遞到最頂層的啟動(dòng)類加載器中。只有當(dāng)父類加載器在自己的加載范圍內(nèi)沒(méi)有搜尋到該類時(shí),并向子類反饋?zhàn)约簾o(wú)法加載后,子類加載器才會(huì)嘗試自己去加載。
ClassLoader內(nèi)的loadClass方法,就很好的解釋了雙親委派的加載模式:
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- //檢查該class是否已經(jīng)被當(dāng)前類加載器加載過(guò)
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- //此時(shí)該class還沒(méi)有被加載
- try {
- if (parent != null) {
- //如果父加載器不為null,則委托給父類加載
- c = parent.loadClass(name, false);
- } else {
- //如果父加載器為null,說(shuō)明當(dāng)前類加載器已經(jīng)是啟動(dòng)類加載器,直接時(shí)候用啟動(dòng)類加載器去加載該class
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- }
- if (c == null) {
- //此時(shí)父類加載器都無(wú)法加載該class,則使用當(dāng)前類加載器進(jìn)行加載
- long t1 = System.nanoTime();
- c = findClass(name);
- ...
- }
- }
- //是否需要連接該類
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
為什么要使用雙親委派機(jī)制,就使用當(dāng)前的類加載器去加載不就行了嗎?為啥搞得這么復(fù)雜呢?
假設(shè)現(xiàn)在并沒(méi)有雙親委派機(jī)制,有這樣的一個(gè)場(chǎng)景:
用戶寫了一個(gè)Student類,點(diǎn)擊運(yùn)行,此時(shí)編譯完成后,虛擬機(jī)開(kāi)始加載class,該class會(huì)由應(yīng)用加載器進(jìn)行加載,由于Object類是Student的父類,且雙親委派機(jī)制不存在的情況下,應(yīng)用加載器就會(huì)自己嘗試加載Object類,但是用戶壓根沒(méi)定義Object,即應(yīng)用加載器無(wú)法在加載范圍搜尋到該類,所以此時(shí)Object類無(wú)法被加載,用戶寫的代碼無(wú)法運(yùn)行。
假設(shè)該用戶自己定義了一個(gè)Object類,此時(shí)再次運(yùn)行后,應(yīng)用類加載器則會(huì)正常加載用戶定義的Object與Student類。Student類中會(huì)調(diào)用System.out.print()輸出Student對(duì)象,此時(shí)會(huì)由啟動(dòng)類加載器加載System類,在此之前同樣也會(huì)加載Object類。
此時(shí),方法區(qū)中有了兩份Object的元數(shù)據(jù),Object類被重復(fù)加載了!
倘若用戶定義的Object類不安全,可能直接造成虛擬機(jī)崩潰或者引起重大安全問(wèn)題。
如果現(xiàn)在使用雙親委派機(jī)制,用戶雖然自己定義了Object類,可以通過(guò)編譯,但是永遠(yuǎn)不會(huì)被記載進(jìn)方法區(qū)。
雙親委派機(jī)制避免了重復(fù)加載,也保證了虛擬機(jī)的安全。
自定義類加載器
我們整理ClassLoader里面的流程
- loadclass:判斷是否已加載,使用雙親委派模型,請(qǐng)求父加載器,父加載器反饋無(wú)法加載,因此使用findclass,讓當(dāng)前類加載器查找
- findclass:當(dāng)前類加載器根據(jù)路徑以及class文件名稱加載字節(jié)碼,從class文件中讀取字節(jié)數(shù)組,然后使用defineClass
- defineclass:根據(jù)字節(jié)數(shù)組,返回Class對(duì)象
我們?cè)贑lassLoader里面找到findClass方法,發(fā)現(xiàn)該方法直接拋出異常,應(yīng)該是留給子類實(shí)現(xiàn)的。
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
到這里,我們應(yīng)該明白,loadClass方法使用了模版方法模式,主線邏輯是雙親委派,但如何將class文件轉(zhuǎn)化為Class對(duì)象的步驟,已經(jīng)交由子類去實(shí)現(xiàn)。對(duì)模版方法模式不熟悉的同學(xué),可以先參考我的另外一篇文章模版方法模式
其實(shí)源碼中,已經(jīng)有一個(gè)自定義類加載的樣例代碼,在注釋中:
- class NetworkClassLoader extends ClassLoader {
- String host;
- int port;
- public Class findClass(String name) {
- byte[] b = loadClassData(name);
- return defineClass(name, b, 0, b.length);
- }
- private byte[] loadClassData(String name) {
- // load the class data from the connection
- }
- }
看得出來(lái),如果我們需要自定義類加載器,只需要繼承ClassLoader,并且重寫findClass方法即可。
現(xiàn)在有一個(gè)簡(jiǎn)單的樣例,class文件依然在文件目錄中:
- package com.yang.testClassLoader;
- import sun.misc.Launcher;
- import java.io.*;
- public class MyClassLoader extends ClassLoader {
- /**
- * 類加載路徑,不包含文件名
- */
- private String path;
- public MyClassLoader(String path) {
- super();
- this.path = path;
- }
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- byte[] bytes = getBytesFromClass(name);
- assert bytes != null;
- //讀取字節(jié)數(shù)組,轉(zhuǎn)化為Class對(duì)象
- return defineClass(name, bytes, 0, bytes.length);
- }
- //讀取class文件,轉(zhuǎn)化為字節(jié)數(shù)組
- private byte[] getBytesFromClass(String name) {
- String absolutePath = path + "/" + name + ".class";
- FileInputStream fis = null;
- ByteArrayOutputStream bos = null;
- try {
- fis = new FileInputStream(new File(absolutePath));
- bos = new ByteArrayOutputStream();
- byte[] temp = new byte[1024];
- int len;
- while ((len = fis.read(temp)) != -1) {
- bos.write(temp, 0, len);
- }
- return bos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (null != fis) {
- try {
- fis.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (null != bos) {
- try {
- bos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- return null;
- }
- public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
- MyClassLoader classLoader = new MyClassLoader("C://develop");
- Class test = classLoader.loadClass("Student");
- test.newInstance();
- }
- }
Student類:
- public class Student {
- public Student() {
- System.out.println("student classloader is" + this.getClass().getClassLoader().toString());
- }
- }
注意,這個(gè)Student類千萬(wàn)不要加包名,idea報(bào)錯(cuò)不管他即可,然后使用javac Student.java編譯該類,將生成的class文件復(fù)制到c://develop下即可。
運(yùn)行MyClassLoader的main方法后,可以看到輸出:

看得出來(lái),Student.class確實(shí)是被我們自定義的類加載器給加載了。
破壞雙親委派
從上面的自定義類加載器的內(nèi)容中,我們應(yīng)該可以猜到了,破壞雙親委派直接重寫loadClass方法就完事了。事實(shí)上,我們確實(shí)可以重寫loadClass方法,畢竟這個(gè)方法沒(méi)有被final修飾。雙親委派既然有好處,為什么jdk對(duì)loadClass開(kāi)放重寫呢?這要從雙親委派引入的時(shí)間來(lái)看:
- 雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時(shí)代就已經(jīng)存在,面對(duì)已經(jīng)存在的用戶自定義類加載器的實(shí)現(xiàn)代碼,Java設(shè)計(jì)者引入雙親委派模型時(shí)不得不做出一些妥協(xié)。在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,jdk為了向前兼容,不得已開(kāi)放對(duì)loadClass的重寫操作。
當(dāng)然,也不止這一次對(duì)雙親委派模型的破壞,詳細(xì)的文章可以參考破壞雙親委派模型,里面提到了一個(gè)“線程上下文類加載器”,對(duì)這個(gè)不熟悉的同學(xué)可以參考真正理解線程上下文類加載器(多案例分析)(無(wú)法放鏈接,百度搜索)
我們經(jīng)常用的Tomcat與jdbc,就破壞了雙親委派,礙于文章的篇幅與博主的水平,暫時(shí)不在這里討論破壞的原因,有興趣的同學(xué)可以參考這一篇文章JDBC、Tomcat為什么要破壞雙親委派模型?(無(wú)法放鏈接,百度搜索)