摘要:本文主要介紹類加載器、自定義類加載器及類的加載和卸載等內容,並舉例介紹了Java類的熱替換。
最近,遇到了兩個和Java類的加載和卸載相關的問題:java
1) 是一道關於Java的判斷題:一個類被首次加載後,會長期留駐JVM,直到JVM退出。這個說法,是否是正確的?ide
2) 在開發的一個集成平臺中,須要集成相似接口的多種工具,而且工具可能會有新增,同時在不一樣的環境部署會有裁剪(例如對外提供服務的應用,不能提供特定的採購的工具),如何才能更好地實現?工具
針對上面的第2點,咱們採用Java插件化開發實現。上面的兩個問題,都和Java的類加載和熱替換機制有關。測試
類加載器,顧名思義,就是用來實現類的加載操做。每一個類加載器都有一個獨立的類名稱空間,就是說每一個由該類加載器加載的類,都在本身的類名稱空間,若是要比較兩個類是否「相等」,首先這兩個類必須在相同的類命名空間,即由相同的類加載器加載(即對於任何一個類,都必須由該類自己和加載它的類加載器一塊兒肯定其在JVM中的惟一性),不是同一個類加載器加載的類,不會相等。this
在Java中,主要有以下的類加載器:idea
圖1.1 Java類加載器spa
下面,簡單介紹上面這幾種類加載器:插件
雙親委派模型,是從 Java1.2 開始引入的一種類加載器模式,在Java中,類的加載操做經過java.lang.ClassLoader中的loadClass()方法完成,我們首先看看該方法的實現(直接從Java源碼中撈出來的):code
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; } }
咱們結合上面的註釋,來解釋下雙親委派模型的內容:orm
1) 接收到一個類加載請求後,首先判斷該類是否有加載,若是已經加載,則直接返回;
2) 若是還沒有加載,首先獲取父類加載器,若是能夠獲取父類加載器,則調用父類的loadClass()方法來加載該類,若是沒法獲取父類加載器,則調用啓動器加載器來加載該類;
3) 判斷該類是否被父類加載器或者啓動類加載器加載,若是已經加載完成則返回,若是未成功加載,則本身嘗試來加載該類。
上面的描述,說明了loadClass()方法的實現,咱們進一步對上面的步驟進行解釋:
最後囉嗦一下,再進行一下總結:
雙親委派模型:若是一個類加載器收到類加載請求,會首先把加載請求委派給父類加載器完成,每一個層次的類加載器都是這樣,最終全部的加載請求都傳動到最根的啓動類加載器來完成,若是父類加載器沒法完成該加載請求(即本身加載的範圍內找不到該類),子類加載器纔會嘗試本身加載。
這樣的雙親委派模型有個好處:就是全部的類都儘量由頂層的類加載器加載,保證了加載的類的惟一性,若是每一個類都隨機由不一樣的類加載器加載,則類的實現關係沒法保證,對於保證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類,是能夠被卸載的。
前面介紹到,類加載的雙親委派模型,是推薦模型,在loadClass中實現的,並非必須使用的模型。咱們能夠經過自定義類加載器,直接加載咱們須要的Java類,而不委託給父類加載器。
圖2.1 自定義類加載器
如上圖所示,咱們有自定義的類加載器MyClassLoader,用來加載類MyClass,則在JVM中,會存在上面三類引用(上圖忽略這三種類型對象對其餘的對象的引用)。若是咱們將左邊的三個引用變量,均設置爲null,那麼此時,已經加載的MyClass將會被卸載。
動態卸載須要藉助於JVM的垃圾收集功能才能夠作到,可是咱們知道,JVM的垃圾回收,只有在堆內存佔用比較高的時候,纔會觸發。即便咱們調用了System.gc(),也不會當即執行垃圾回收操做,而只是告訴JVM須要執行垃圾回收,至於何時垃圾回收,則要看JVM本身的垃圾回收策略。
可是咱們不須要悲觀,即便動態卸載不是那麼牢靠,可是實現動態的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...
本文主要介紹類加載器、自定義類加載器及類的加載和卸載等內容,並舉例介紹了Java類的熱替換實現。
其實,最近在開發項目中,須要裁剪特性,就想用pf4j來作插件化開發,瞭解了一些類加載機制,整理一下。
主要參考《深刻Java虛擬機:JVM高級特性與最佳實踐》。
本文分享自華爲雲社區《Java類動態加載和熱替換》,原文做者:maijun 。