類加載機制是 Java 語言的一大亮點,使得 Java 類能夠被動態加載到 Java 虛擬機中。java
此次咱們拋開術語和概念,從例子入手,由淺入深地講解 Java 的類加載機制。git
本文涉及知識點:雙親委託機制、BootstrapClassLoader、ExtClassLoader、AppClassLoader、自定義網絡類加載器等github
文章涉及代碼:GitHub地址apache
個人更多文章:《Android 開發進階》bash
Java 虛擬機通常使用 Java 類的流程爲:首先將開發者編寫的 Java 源代碼(.java文件)編譯成 Java 字節碼(.class文件),而後類加載器會讀取這個 .class 文件,並轉換成 java.lang.Class 的實例。有了該 Class 實例後,Java 虛擬機能夠利用 newInstance 之類的方法建立其真正對象了。服務器
ClassLoader 是 Java 提供的類加載器,絕大多數的類加載器都繼承自 ClassLoader,它們被用來加載不一樣來源的 Class 文件。markdown
上文提到了 ClassLoader 能夠去加載多種來源的 Class,那麼具體有哪些來源呢?網絡
首先,最多見的是開發者在應用程序中編寫的類,這些類位於項目目錄下;app
而後,有 Java 內部自帶的核心類
如 java.lang
、java.math
、java.io
等 package 內部的類,位於 $JAVA_HOME/jre/lib/
目錄下,如 java.lang.String
類就是定義在 $JAVA_HOME/jre/lib/rt.jar
文件裏;ide
另外,還有 Java 核心擴展類
,位於 $JAVA_HOME/jre/lib/ext
目錄下。開發者也能夠把本身編寫的類打包成 jar 文件放入該目錄下;
最後還有一種,是動態加載遠程的 .class 文件。
既然有這麼多種類的來源,那麼在 Java 裏,是由某一個具體的 ClassLoader 來統一加載呢?仍是由多個 ClassLoader 來協做加載呢?
實際上,針對上面四種來源的類,分別有不一樣的加載器負責加載。
首先,咱們來看級別最高的 Java 核心類
,即$JAVA_HOME/jre/lib
裏的核心 jar 文件。這些類是 Java 運行的基礎類,由一個名爲 BootstrapClassLoader
加載器負責加載,它也被稱做 根加載器/引導加載器
。注意,BootstrapClassLoader
比較特殊,它不繼承 ClassLoader
,而是由 JVM 內部實現;
而後,須要加載 Java 核心擴展類
,即 $JAVA_HOME/jre/lib/ext
目錄下的 jar 文件。這些文件由 ExtensionClassLoader
負責加載,它也被稱做 擴展類加載器
。固然,用戶若是把本身開發的 jar 文件放在這個目錄,也會被 ExtClassLoader
加載;
接下來是開發者在項目中編寫的類,這些文件將由 AppClassLoader
加載器進行加載,它也被稱做 系統類加載器 System ClassLoader
;
最後,若是想遠程加載如(本地文件/網絡下載)的方式,則必需要本身自定義一個 ClassLoader,複寫其中的 findClass()
方法才能得以實現。
所以能看出,Java 裏提供了至少四類 ClassLoader
來分別加載不一樣來源的 Class。
那麼,這幾種 ClassLoader 是如何協做來加載一個類呢?
String 類是 Java 自帶的最經常使用的一個類,如今的問題是,JVM 將以何種方式把 String class 加載進來呢?
咱們來猜測下。
首先,String 類屬於 Java 核心類,位於 $JAVA_HOME/jre/lib
目錄下。有的朋友會立刻反應過來,上文中提過了,該目錄下的類會由 BootstrapClassLoader
進行加載。沒錯,它確實是由 BootstrapClassLoader
進行加載。但,這種回答的前提是你已經知道了 String 在 $JAVA_HOME/jre/lib
目錄下。
那麼,若是你並不知道 String 類究竟位於哪呢?或者我但願你去加載一個 unknown
的類呢?
有的朋友這時會說,那很簡單,只要去遍歷一遍全部的類,看看這個 unknown
的類位於哪裏,而後再用對應的加載器去加載。
是的,思路很正確。那應該如何去遍歷呢?
好比,能夠先遍歷用戶本身寫的類,若是找到了就用 AppClassLoader
去加載;不然去遍歷 Java 核心類目錄,找到了就用 BootstrapClassLoader
去加載,不然就去遍歷 Java 擴展類庫,依次類推。
這種思路方向是正確的,不過存在一個漏洞。
假如開發者本身僞造了一個 java.lang.String
類,即在項目中建立一個包java.lang
,包內建立一個名爲 String
的類,這徹底能夠作到。那若是利用上面的遍歷方法,是否是這個項目中用到的 String 不是都變成了這個僞造的 java.lang.String
類嗎?如何解決這個問題呢?
解決方法很簡單,當查找一個類時,優先遍歷最高級別的 Java 核心類,而後再去遍歷 Java 核心擴展類,最後再遍歷用戶自定義類,並且這個遍歷過程是一旦找到就當即中止遍歷。
在 Java 中,這種實現方式也稱做 雙親委託
。其實很簡單,把 BootstrapClassLoader
想象爲核心高層領導人, ExtClassLoader
想象爲中層幹部, AppClassLoader
想象爲普通公務員。每次須要加載一個類,先獲取一個系統加載器 AppClassLoader
的實例(ClassLoader.getSystemClassLoader()),而後向上級層層請求,由最上級優先去加載,若是上級以爲這些類不屬於核心類,就能夠下放到各子級負責人去自行加載。
以下圖所示:
雙親委託
方式進行類加載嗎?下面經過幾個例子來驗證上面的加載方式。
AppClassLoader
加載嗎?在項目中建立一個名爲 MusicPlayer
的類文件,內容以下:
package classloader; public class MusicPlayer { public void print() { System.out.printf("Hi I'm MusicPlayer"); } } 複製代碼
而後來加載 MusicPlayer
。
private static void loadClass() throws ClassNotFoundException { Class<?> clazz = Class.forName("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName()); } 複製代碼
打印結果爲:
ClassLoader is AppClassLoader
複製代碼
能夠驗證,MusicPlayer
是由 AppClassLoader
進行的加載。
AppClassLoader
的雙親真的是 ExtClassLoader 和 BootstrapClassLoader 嗎?這時發現 AppClassLoader
提供了一個 getParent()
的方法,來打印看看都是什麼。
private static void printParent() throws ClassNotFoundException { Class<?> clazz = Class.forName("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("currentClassLoader is %s\n", classLoader.getClass().getSimpleName()); while (classLoader.getParent() != null) { classLoader = classLoader.getParent(); System.out.printf("Parent is %s\n", classLoader.getClass().getSimpleName()); } } 複製代碼
打印結果爲:
currentClassLoader is AppClassLoader
Parent is ExtClassLoader
複製代碼
首先能看到 ExtClassLoader
確實是 AppClassLoader
的雙親,不過卻沒有看到 BootstrapClassLoader
。事實上,上文就提過, BootstrapClassLoader
比較特殊,它是由 JVM 內部實現的,因此 ExtClassLoader.getParent() = null
。
$JAVA_HOME/jre/lib/ext
目錄下會發生什麼?上文中說了,ExtClassLoader
會加載$JAVA_HOME/jre/lib/ext
目錄下全部的 jar 文件。那來嘗試下直接把 MusicPlayer
這個類放到 $JAVA_HOME/jre/lib/ext
目錄下吧。
利用下面命令能夠把 MusicPlayer.java 編譯打包成 jar 文件,並放置到對應目錄。
javac classloader/MusicPlayer.java jar cvf MusicPlayer.jar classloader/MusicPlayer.class mv MusicPlayer.jar $JAVA_HOME/jre/lib/ext/ 複製代碼
這時 MusicPlayer.jar 已經被放置與 $JAVA_HOME/jre/lib/ext
目錄下,同時把以前的 MusicPlayer
刪除
,並且這一次刻意
使用 AppClassLoader
來加載:
private static void loadClass() throws ClassNotFoundException { ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // AppClassLoader Class<?> clazz = appClassLoader.loadClass("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName()); } 複製代碼
打印結果爲:
ClassLoader is ExtClassLoader
複製代碼
說明即便直接用 AppClassLoader
去加載,它仍然會被 ExtClassLoader
加載到。
雙親委託
加載機制上面已經經過一些例子瞭解了雙親委託
的一些特性了,下面來看一下它的實現代碼,加深理解。
打開 ClassLoader
裏的 loadClass()
方法,即是須要分析的源碼了。這個方法裏作了下面幾件事:
代碼以下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 檢查是否曾加載過 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 優先讓 parent 加載器去加載 c = parent.loadClass(name, false); } else { // 如無 parent,表示當前是 BootstrapClassLoader,調用 native 方法去 JVM 加載 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 若是 parent 均沒有加載到目標class,調用自身的 findClass() 方法去搜索 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // BootstrapClassLoader 會調用 native 方法去 JVM 加載 private native Class<?> findBootstrapClass(String name); 複製代碼
看完實現源碼相信可以有更完整的理解。
前面提到了 Java 自帶的加載器 BootstrapClassLoader
、AppClassLoader
和ExtClassLoader
,這些都是 Java 已經提供好的。
而真正有意思的,是 自定義類加載器
,它容許咱們在運行時
能夠從本地磁盤或網絡
上動態加載自定義類。這使得開發者能夠動態修復某些有問題的類,熱更新代碼。
下面來實現一個網絡類加載器
,這個加載器能夠從網絡上動態下載 .class 文件並加載到虛擬機中使用。
後面我還會寫做與 熱修復/動態更新
相關的文章,這裏先學習 Java 層 NetworkClassLoader
相關的原理。
NetworkClassLoader
,它首先要繼承 ClassLoader
;ClassLoader
內的 findClass()
方法。注意,不是loadClass()
方法,由於ClassLoader
提供了loadClass()
(如上面的源碼),它會基於雙親委託
機制去搜索某個 class,直到搜索不到纔會調用自身的findClass()
,若是直接複寫loadClass()
,那還要實現雙親委託
機制;findClass()
方法裏,要從網絡上下載一個 .class 文件,而後轉化成 Class 對象供虛擬機使用。具體實現代碼以下:
/** * Load class from network */ public class NetworkClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = downloadClassData(name); // 從遠程下載 if (classData == null) { super.findClass(name); // 未找到,拋異常 } else { return defineClass(name, classData, 0, classData.length); // convert class byte data to Class<?> object } return null; } private byte[] downloadClassData(String name) { // 從 localhost 下載 .class 文件 String path = "http://localhost" + File.separatorChar + "java" + File.separatorChar + name.replace('.', File.separatorChar) + ".class"; try { URL url = new URL(path); InputStream ins = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); // 把下載的二進制數據存入 ByteArrayOutputStream } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; } public String getName() { System.out.printf("Real NetworkClassLoader\n"); return "networkClassLoader"; } } 複製代碼
這個類的做用是從網絡上(這裏是本人的 local apache 服務器 http://localhost/java 上)目錄裏去下載對應的 .class 文件,並轉換成 Class<?> 返回回去使用。
下面咱們來利用這個 NetworkClassLoader
去加載 localhost 上的 MusicPlayer
類:
MusicPlayer.class
放置於 /Library/WebServer/Documents/java
(MacOS)目錄下,因爲 MacOS 自帶 apache 服務器,這裏是服務器的默認目錄;String className = "classloader.NetworkClass"; NetworkClassLoader networkClassLoader = new NetworkClassLoader(); Class<?> clazz = networkClassLoader.loadClass(className); 複製代碼
http://localhost/java/classloader/MusicPlayer.class
成功。能夠看出 NetworkClassLoader
能夠正常工做,若是讀者要用的話,只要稍微修改 url 的拼接方式便可自行使用。
類加載方式是 Java 上很是創新的一項技術,給將來的熱修復技術提供了可能。本文力求經過簡單的語言和合適的例子來說解其中雙親委託機制
、自定義加載器
等,並開發了自定義的NetworkClassLoader
。
固然,類加載是頗有意思的技術,很難覆蓋全部知識點,好比不一樣類加載器加載同一個類,獲得的實例卻不是同一個等等。
謝謝。
wingjay