類載入器的型別
類載入器有以下種類:
啟動類載入器(Bootstrap ClassLoader)擴充套件類載入器(Extension ClassLoader)應用類載入器(Application ClassLoader)啟動類載入器
內嵌在JVM核心中的載入器,由C++語言編寫(因此也不會繼承ClassLoader),是類載入器層次中最頂層的載入器。用於載入java的核心類庫,即載入jre/lib/rt.jar裡所有的class。由於啟動類載入器涉及到虛擬機器本地實現細節,我們無法獲取啟動類載入器的引用。
擴充套件類載入器
它負責載入JRE的擴充套件目錄,jre/lib/ext或者由java.ext.dirs系統屬性指定的目錄中jar包的類。父類載入器為啟動類載入器,但使用擴充套件類載入器呼叫getParent依然為null。
應用類載入器
又稱系統類載入器,可用透過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類載入器的例項,系統類載入器也因此得名。應用類載入器主要載入classpath下的class,即使用者自己編寫的應用編譯得來的class,呼叫getParent返回擴充套件類載入器。
擴充套件類載入器與應用類載入器繼承結構如圖所示:
可以看到除了啟動類載入器,其餘的兩個類載入器都繼承於ClassLoader,我們自定義的類載入,也需要繼承ClassLoader。
雙親委派機制當一個類載入器收到了一個類載入請求時,它自己不會先去嘗試載入這個類,而是把這個請求轉交給父類載入器,每一個層的類載入器都是如此,因此所有的類載入請求都應該傳遞到最頂層的啟動類載入器中。只有當父類載入器在自己的載入範圍內沒有搜尋到該類時,並向子類反饋自己無法載入後,子類載入器才會嘗試自己去載入。
ClassLoader內的loadClass方法,就很好的解釋了雙親委派的載入模式:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //檢查該class是否已經被當前類載入器載入過 Class<?> c = findLoadedClass(name); if (c == null) { //此時該class還沒有被載入 try { if (parent != null) { //如果父載入器不為null,則委託給父類載入 c = parent.loadClass(name, false); } else { //如果父載入器為null,說明當前類載入器已經是啟動類載入器,直接時候用啟動類載入器去載入該class c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { //此時父類載入器都無法載入該class,則使用當前類載入器進行載入 long t1 = System.nanoTime(); c = findClass(name); ... } } //是否需要連線該類 if (resolve) { resolveClass(c); } return c; } }
為什麼要使用雙親委派機制,就使用當前的類載入器去載入不就行了嗎?為啥搞得這麼複雜呢?
假設現在並沒有雙親委派機制,有這樣的一個場景:
使用者寫了一個Student類,點選執行,此時編譯完成後,虛擬機器開始載入class,該class會由應用載入器進行載入,由於Object類是Student的父類,且雙親委派機制不存在的情況下,應用載入器就會自己嘗試載入Object類,但是使用者壓根沒定義Object,即應用載入器無法在載入範圍搜尋到該類,所以此時Object類無法被載入,使用者寫的程式碼無法執行。
假設該使用者自己定義了一個Object類,此時再次執行後,應用類載入器則會正常載入使用者定義的Object與Student類。Student類中會呼叫System.out.print()輸出Student物件,此時會由啟動類載入器載入System類,在此之前同樣也會載入Object類。
此時,方法區中有了兩份Object的元資料,Object類被重複載入了!
倘若使用者定義的Object類不安全,可能直接造成虛擬機器崩潰或者引起重大安全問題。
如果現在使用雙親委派機制,使用者雖然自己定義了Object類,可以透過編譯,但是永遠不會被記載進方法區。
雙親委派機制避免了重複載入,也保證了虛擬機器的安全。
自定義類載入器我們整理ClassLoader裡面的流程
loadclass:判斷是否已載入,使用雙親委派模型,請求父載入器,父載入器反饋無法載入,因此使用findclass,讓當前類載入器查詢findclass:當前類載入器根據路徑以及class檔名稱載入位元組碼,從class檔案中讀取位元組陣列,然後使用defineClassdefineclass:根據位元組陣列,返回Class物件我們在ClassLoader裡面找到findClass方法,發現該方法直接丟擲異常,應該是留給子類實現的。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
到這裡,我們應該明白,loadClass方法使用了模版方法模式,主線邏輯是雙親委派,但如何將class檔案轉化為Class物件的步驟,已經交由子類去實現。對模版方法模式不熟悉的同學,可以先參考我的另外一篇文章模版方法模式
其實原始碼中,已經有一個自定義類載入的樣例程式碼,在註釋中:
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 } }
看得出來,如果我們需要自定義類載入器,只需要繼承ClassLoader,並且重寫findClass方法即可。
現在有一個簡單的樣例,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; //讀取位元組陣列,轉化為Class物件 return defineClass(name, bytes, 0, bytes.length); } //讀取class檔案,轉化為位元組陣列 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()); }}
注意,這個Student類千萬不要加包名,idea報錯不管他即可,然後使用javac Student.java編譯該類,將生成的class檔案複製到c://develop下即可。
執行MyClassLoader的main方法後,可以看到輸出:
看得出來,Student.class確實是被我們自定義的類載入器給載入了。
破壞雙親委派從上面的自定義類載入器的內容中,我們應該可以猜到了,破壞雙親委派直接重寫loadClass方法就完事了。事實上,我們確實可以重寫loadClass方法,畢竟這個方法沒有被final修飾。雙親委派既然有好處,為什麼jdk對loadClass開放重寫呢?這要從雙親委派引入的時間來看:
雙親委派模型是在JDK1.2之後才被引入的,而類載入器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的使用者自定義類載入器的實現程式碼,Java設計者引入雙親委派模型時不得不做出一些妥協。在此之前,使用者去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,jdk為了向前相容,不得已開放對loadClass的重寫操作。
當然,也不止這一次對雙親委派模型的破壞,詳細的文章可以參考破壞雙親委派模型,裡面提到了一個“執行緒上下文類載入器”,對這個不熟悉的同學可以參考真正理解執行緒上下文類載入器(多案例分析)(無法放連結,百度搜索)