摘要:本文主要介紹類載入器、自定義類載入器及類的載入和解除安裝等內容,並舉例介紹了Java類的熱替換。
最近,遇到了兩個和Java類的載入和解除安裝相關的問題:
1) 是一道關於Java的判斷題:一個類被首次載入後,會長期留駐JVM,直到JVM退出。這個說法,是不是正確的?
2) 在開發的一個整合平臺中,需要整合類似介面的多種工具,並且工具可能會有新增,同時在不同的環境部署會有裁剪(例如對外提供服務的應用,不能提供特定的採購的工具),如何才能更好地實現?
針對上面的第2點,我們採用Java外掛化開發實現。上面的兩個問題,都和Java的類載入和熱替換機制有關。
1. Java的類載入器和雙親委派模型
1.1 Java類載入器
類載入器,顧名思義,就是用來實現類的載入操作。每個類載入器都有一個獨立的類名稱空間,就是說每個由該類載入器載入的類,都在自己的類名稱空間,如果要比較兩個類是否“相等”,首先這兩個類必須在相同的類名稱空間,即由相同的類載入器載入(即對於任何一個類,都必須由該類本身和載入它的類載入器一起確定其在JVM中的唯一性),不是同一個類載入器載入的類,不會相等。
在Java中,主要有如下的類載入器:
圖1.1 Java類載入器
下面,簡單介紹上面這幾種類載入器:
- 啟動類載入器(Bootstrap Class Loader):這個類使用C++開發(所有的類載入器中,唯一使用C++開發的類載入器),用來載入<JAVA_HOME>/lib目錄中jar和tools.jar或者使用 -Xbootclasspath 引數指定的類。
- 擴充套件類載入器(Extension Class Loader):定義為misc.Launcher$ExtClassLoader,用來載入<JAVA_HOME>/lib/ext目錄或者使用java.ext.dir指定的類。
- 應用程式類載入器(Application Class Loader):定義為misc.Launcher$AppClassLoader,用來載入使用者類路徑下面(classpath)下面所有的類,一般情況下,該類是應用程式預設的類載入器。
- 使用者自定義類載入器(User Class Loader):使用者自定義類載入器,一般沒有必要,後面我們會專門來一部分介紹該型別的類載入器。
1.2 雙親委派模型
雙親委派模型,是從 Java1.2 開始引入的一種類載入器模式,在Java中,類的載入操作通過java.lang.ClassLoader中的loadClass()方法完成,我們們首先看看該方法的實現(直接從Java原始碼中撈出來的):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. 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; } }
我們結合上面的註釋,來解釋下雙親委派模型的內容:
1) 接收到一個類載入請求後,首先判斷該類是否有載入,如果已經載入,則直接返回;
2) 如果尚未載入,首先獲取父類載入器,如果可以獲取父類載入器,則呼叫父類的loadClass()方法來載入該類,如果無法獲取父類載入器,則呼叫啟動器載入器來載入該類;
3) 判斷該類是否被父類載入器或者啟動類載入器載入,如果已經載入完成則返回,如果未成功載入,則自己嘗試來載入該類。
上面的描述,說明了loadClass()方法的實現,我們進一步對上面的步驟進行解釋:
- 因為類載入器首先調父類載入器來進行載入,從loadClass()方法的實現,我們知道父類載入器會嘗試調自己的父類載入器,直到啟動類載入器,所以,任何一個類的載入,都會最終委託到啟動類載入器來首先載入;
- 在前面有進行介紹,啟動類載入器、擴充套件類載入器、應用程式類載入器,都有自己載入的類的範圍,例如啟動類載入器只載入JDK核心庫,因此並不是父類載入器就可以都載入成功,父類載入器無法載入(一般如上面程式碼,丟擲來ClassNotFoundException),此時會由自己載入。
最後囉嗦一下,再進行一下總結:
雙親委派模型:如果一個類載入器收到類載入請求,會首先把載入請求委派給父類載入器完成,每個層次的類載入器都是這樣,最終所有的載入請求都傳動到最根的啟動類載入器來完成,如果父類載入器無法完成該載入請求(即自己載入的範圍內找不到該類),子類載入器才會嘗試自己載入。
這樣的雙親委派模型有個好處:就是所有的類都儘可能由頂層的類載入器載入,保證了載入的類的唯一性,如果每個類都隨機由不同的類載入器載入,則類的實現關係無法保證,對於保證Java程式的穩定執行意義重大。
2. Java的類動態載入和解除安裝
2.1 Java類的解除安裝
在Java中,每個類都有相應的Class Loader,同樣的,每個例項物件也會有相應的類,當滿足如下三個條件時,JVM就會解除安裝這個類:
1) 該類所有例項物件不可達
2) 該類的Class物件不可達
3) 該類的Class Loader不可達
那麼,上面示例物件、Class物件和類的Class Loader直接是什麼關係呢?
在類載入器的內部實現中,用一個Java集合來存放所載入類的引用。而一個Class物件總是會引用它的類載入器,呼叫Class物件的getClassLoader()方法,就能獲得它的類載入器。所以,Class例項和載入它的載入器之間為雙向引用關係。
一個類的例項總是引用代表這個類的Class物件。在Object類中定義了getClass()方法,這個方法返回代表物件所屬類的Class物件的引用。此外,所有的Java類都有一個靜態屬性class,它引用代表這個類的Class物件。
Java虛擬機器自帶的類載入器(前面介紹的三種類載入器)在JVM執行過程中,會始終存在,而這些類載入器則會始終引用它們所載入的類的Class物件,因此這些Class物件始終是可觸及的。因此,由Java虛擬機器自帶的類載入器所載入的類,在虛擬機器的生命週期中,始終不會被解除安裝。
那麼,我們是不是就完全不能在Java程式執行過程中,動態修改我們使用的類了嗎?答案是否定的!根據上面的分析,通過Java虛擬機器自帶的類載入器載入的類無法解除安裝,我們可以自定義類載入器來載入Java程式,通過自定義類載入器載入的Java類,是可以被解除安裝的。
2.2 自定義類載入器
前面介紹到,類載入的雙親委派模型,是推薦模型,在loadClass中實現的,並不是必須使用的模型。我們可以通過自定義類載入器,直接載入我們需要的Java類,而不委託給父類載入器。
圖2.1 自定義類載入器
如上圖所示,我們有自定義的類載入器MyClassLoader,用來載入類MyClass,則在JVM中,會存在上面三類引用(上圖忽略這三種型別物件對其他的物件的引用)。如果我們將左邊的三個引用變數,均設定為null,那麼此時,已經載入的MyClass將會被解除安裝。
2.3 動態解除安裝存在的問題
動態解除安裝需要藉助於JVM的垃圾收集功能才可以做到,但是我們知道,JVM的垃圾回收,只有在堆記憶體佔用比較高的時候,才會觸發。即使我們呼叫了System.gc(),也不會立即執行垃圾回收操作,而只是告訴JVM需要執行垃圾回收,至於什麼時候垃圾回收,則要看JVM自己的垃圾回收策略。
但是我們不需要悲觀,即使動態解除安裝不是那麼牢靠,但是實現動態的Java類的熱替換還是有希望的。
3. Java類的熱替換
下面通過程式碼來介紹Java類的熱替換方法(程式碼簡陋,主要為了說明問題):
如下面的程式碼:
首先定義一個自定義類載入器:
package zmj; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class FileClassLoader extends ClassLoader { private String fileName; public void setFileName(String fileName) { this.fileName = fileName; } public Class loadClass(String name) throws ClassNotFoundException { if (name.startsWith("java")) { return getSystemClassLoader().loadClass(name); } Class cls = null; File classF = new File(fileName); try { cls = instantiateClass(name, new FileInputStream(classF), classF.length()); } catch (IOException e) { e.printStackTrace(); } return cls; } private Class instantiateClass(String name, InputStream fin, long len) throws IOException { byte[] raw = new byte[(int) len]; fin.read(raw); fin.close(); return defineClass(name, raw, 0, raw.length); } }
上面在loadClass時,先判斷類name(包含package的全限定名)是否以java開始,如果是java開始,則使用JVM自帶的類載入器載入。
然後定義一個簡單的動態載入類:
package zmj; public class SayHello { public void say() { System.out.println("hello ping..."); } }
在執行過程中,會動態修改列印內容,測試類的熱載入。
然後定義一個呼叫類:
package zmj; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws InterruptedException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { while (true) { FileClassLoader fileClassLoader = new FileClassLoader(); fileClassLoader.setFileName("D:/workspace/idea/test/class-loader-test/target/classes/zmj/SayHello.class"); Object obj = null; obj = fileClassLoader.loadClass("zmj.SayHello").newInstance(); Method m = obj.getClass().getMethod("say", new Class[]{}); m.invoke(obj, new Object[]{}); Thread.sleep(2000); } } }
當我們執行上面Main程式過程中,我們動態修改執行內容(SayHello中,從 hello zmj... 更改為 hello ping...),最終展示的內容如下:
hello zmj...
hello zmj...
hello zmj...
hello ping...
hello ping...
hello ping...
4. 總結
本文主要介紹類載入器、自定義類載入器及類的載入和解除安裝等內容,並舉例介紹了Java類的熱替換實現。
其實,最近在開發專案中,需要裁剪特性,就想用pf4j來做外掛化開發,瞭解了一些類載入機制,整理一下。
主要參考《深入Java虛擬機器:JVM高階特性與最佳實踐》。
本文分享自華為雲社群《Java類動態載入和熱替換》,原文作者:maijun 。