Java 技術之類載入機制
類載入機制是 Java 語言的一大亮點,使得 Java 類可以被動態載入到 Java 虛擬機器中。
這次我們拋開術語和概念,從例子入手,由淺入深地講解 Java 的類載入機制。
本文涉及知識點:雙親委託機制、BootstrapClassLoader、ExtClassLoader、AppClassLoader、自定義網路類載入器等
文章涉及程式碼:
什麼是 Java 類載入機制?
Java 虛擬機器一般使用 Java 類的流程為:首先將開發者編寫的 Java 原始碼(.java 檔案)編譯成 Java 位元組碼(.class 檔案),然後類載入器會讀取這個 .class 檔案,並轉換成 java.lang.Class 的例項。有了該 Class 例項後,Java 虛擬機器可以利用 newInstance 之類的方法建立其真正物件了。
ClassLoader 是 Java 提供的類載入器,絕大多數的類載入器都繼承自 ClassLoader,它們被用來載入不同來源的 Class 檔案。
Class 檔案有哪些來源呢?
上文提到了 ClassLoader 可以去載入多種來源的 Class,那麼具體有哪些來源呢?
首先,最常見的是開發者在應用程式中編寫的類,這些類位於專案目錄下;
然後,有 Java 內部自帶的 核心類
如 java.lang
、java.math
、java.io
等 package 內部的類,位於 $JAVA_HOME/jre/lib/
目錄下,如 java.lang.String
類就是定義在 $JAVA_HOME/jre/lib/rt.jar
檔案裡;
另外,還有 Java 核心擴充套件類
,位於 $JAVA_HOME/jre/lib/ext
目錄下。開發者也可以把自己編寫的類打包成 jar 檔案放入該目錄下;
最後還有一種,是動態載入遠端的 .class 檔案。
既然有這麼多種類的來源,那麼在 Java 裡,是由某一個具體的 ClassLoader 來統一載入呢?還是由多個 ClassLoader 來協作載入呢?
哪些 ClassLoader 負責載入上面幾類 Class?
實際上,針對上面四種來源的類,分別有不同的載入器負責載入。
首先,我們來看級別最高的 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 是如何協作來載入一個類呢?
這些 ClassLoader 以何種方式來協作載入 String 類呢?
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
的類檔案,內容如下:
1 2 3 4 5 6 7 |
package classloader; public class MusicPlayer { public void print() { System.out.printf("Hi I'm MusicPlayer"); } } |
然後來載入 MusicPlayer
。
1 2 3 4 5 |
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()); } |
列印結果為:
1 |
ClassLoader is AppClassLoader |
可以驗證,MusicPlayer
是由 AppClassLoader
進行的載入。
驗證 AppClassLoader
的雙親真的是 ExtClassLoader 和 BootstrapClassLoader 嗎?
這時發現 AppClassLoader
提供了一個 getParent()
的方法,來列印看看都是什麼。
1 2 3 4 5 6 7 8 9 10 |
private static void printParent() throws ClassNotFoundException { Class> clazz = Class.forName("classloader.MusicPlayer"); ClassLoader classLoader = clazz.getClassLoader(); System.out.printf("currentClassLoader is %sn", classLoader.getClass().getSimpleName()); while (classLoader.getParent() != null) { classLoader = classLoader.getParent(); System.out.printf("Parent is %sn", classLoader.getClass().getSimpleName()); } } |
列印結果為:
1 2 |
currentClassLoader is AppClassLoader Parent is ExtClassLoader |
首先能看到 ExtClassLoader
確實是 AppClassLoader
的雙親,不過卻沒有看到 BootstrapClassLoader
。事實上,上文就提過, BootstrapClassLoader
比較特殊,它是由 JVM 內部實現的,所以 ExtClassLoader.getParent() = null
。
如果把 MusicPlayer 類挪到 $JAVA_HOME/jre/lib/ext
目錄下會發生什麼?
上文中說了,ExtClassLoader
會載入$JAVA_HOME/jre/lib/ext
目錄下所有的 jar 檔案。那來嘗試下直接把 MusicPlayer
這個類放到 $JAVA_HOME/jre/lib/ext
目錄下吧。
利用下面命令可以把 MusicPlayer.java 編譯打包成 jar 檔案,並放置到對應目錄。
1 2 3 |
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
來載入:
1 2 3 4 5 6 |
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()); } |
列印結果為:
1 |
ClassLoader is ExtClassLoader |
說明即使直接用 AppClassLoader
去載入,它仍然會被 ExtClassLoader
載入到。
從原始碼角度真正理解 雙親委託
載入機制
上面已經透過一些例子瞭解了 雙親委託
的一些特性了,下面來看一下它的實現程式碼,加深理解。
開啟 ClassLoader
裡的 loadClass()
方法,便是需要分析的原始碼了。這個方法裡做了下面幾件事:
檢查目標 class 是否曾經載入過,如果載入過則直接返回;
如果沒載入過,把載入請求傳遞給 parent 載入器去載入;
如果 parent 載入器載入成功,則直接返回;
如果 parent 未載入到,則自身呼叫 findClass() 方法進行尋找,並把尋找結果返回。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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 物件供虛擬機器使用。
具體實現程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
/** * 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 = "" + 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 NetworkClassLoadern"); return "networkClassLoader"; } } |
這個類的作用是從網路上(這裡是本人的 local apache 伺服器 上)目錄裡去下載對應的 .class 檔案,並轉換成 Class> 返回回去使用。
下面我們來利用這個 NetworkClassLoader
去載入 localhost 上的 MusicPlayer
類:
首先把
MusicPlayer.class
放置於/Library/WebServer/Documents/java
(MacOS)目錄下,由於 MacOS 自帶 apache 伺服器,這裡是伺服器的預設目錄;-
執行下面一段程式碼:
1 2 3
String className = "classloader.NetworkClass"; NetworkClassLoader networkClassLoader = new NetworkClassLoader(); Class> clazz = networkClassLoader.loadClass(className);
正常執行,載入
/java/classloader/MusicPlayer.class
成功。
可以看出 NetworkClassLoader
可以正常工作,如果讀者要用的話,只要稍微修改 url 的拼接方式即可自行使用。
小結
類載入方式是 Java 上非常創新的一項技術,給未來的熱修復技術提供了可能。本文力求透過簡單的語言和合適的例子來講解其中 雙親委託機制
、 自定義載入器
等,並開發了自定義的NetworkClassLoader
。
當然,類載入是很有意思的技術,很難覆蓋所有知識點,比如不同類載入器載入同一個類,得到的例項卻不是同一個等等。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4692/viewspace-2814139/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java核心技術梳理-類載入機制與反射Java反射
- java類載入機制Java
- Java 虛擬機器之四:Java類載入機制Java虛擬機
- Java安全基礎之Java反射機制和ClassLoader類載入機制Java反射
- Java面試題之Java類載入機制詳解!Java面試題
- Java類載入機制總結Java
- 談談 Java 類載入機制Java
- Java基礎篇—Java類載入機制Java
- java虛擬機器類載入機制Java虛擬機
- Java 虛擬機器類載入機制Java虛擬機
- Java基礎-類載入器以及載入機制Java
- Java類載入機制-雙親委派Java
- JVM之類載入機制總結JVM
- 類載入機制
- Java虛擬機器(六):類載入機制Java虛擬機
- Java類載入機制詳解【java面試題】Java面試題
- 虛擬機器類載入機制:類載入時機虛擬機
- JVM(三)-java虛擬機器類載入機制JVMJava虛擬機
- 類的載入機制
- JVM:類載入機制JVM
- JVM類載入機制JVM
- java框架基礎技術之--------反射機制Java框架反射
- JVM 第三篇:Java 類載入機制JVMJava
- 一文學會 Java 類載入機制Java
- 探祕類載入器和類載入機制
- Java高階篇——深入淺出Java類載入機制Java
- 深入理解Java虛擬機器 - 類載入機制Java虛擬機
- 深入理解Java虛擬機器(類載入機制)Java虛擬機
- 深入理解Java虛擬機器 --- 類載入機制Java虛擬機
- 【Java虛擬機器規範】JVM類載入機制Java虛擬機JVM
- 類載入機制總結
- 類載入機制與反射反射
- JVM-類載入機制JVM
- Tomcat類載入機制探析Tomcat
- 虛擬機器類載入機制_類載入的過程虛擬機
- 類載入流程,類載入機制及自定義類載入器
- 自定義類載入器驗證類載入機制
- 十二、java知識點——類載入機制(硬貨)Java