一、JVM中的類載入器類型 從Java虛擬機的角度講,只有兩種不同的類載入器:啟動類載入器和其他類載入器。 1.啟動類載入器(Boostrap ClassLoader):這個是由c++實現的,主要負責JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar ...
一、JVM中的類載入器類型
從Java虛擬機的角度講,只有兩種不同的類載入器:啟動類載入器和其他類載入器。
1.啟動類載入器(Boostrap ClassLoader):這個是由c++實現的,主要負責JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作。
2.其他類載入器:由java實現,可以在方法區找到其Class對象。這裡又細分為幾個載入器
a).擴展類載入器(Extension ClassLoader):負責用於載入JAVA_HOME/lib/ext目錄中的,或者被-Djava.ext.dirs系統變數指定所指定的路徑中所有類庫(jar),開發者可以直接使用擴展類載入器。java.ext.dirs系統變數所指定的路徑的可以通過System.getProperty("java.ext.dirs")來查看。
b).應用程式類載入器(Application ClassLoader):負責java -classpath或-Djava.class.path所指的目錄下的類與jar包裝入工作。開發者可以直接使用這個類載入器。在沒有指定自定義類載入器的情況下,這就是程式的預設載入器。
c).自定義類載入器(User ClassLoader):在程式運行期間, 通過java.lang.ClassLoader的子類動態載入class文件, 體現java動態實時類裝入特性。
這四個類載入器的層級關係,如下圖所示。
二、為什麼要自定義類載入器
- 區分同名的類:假定在tomcat 應用伺服器,上面部署著許多獨立的應用,同時他們擁有許多同名卻不同版本的類。要區分不同版本的類當然是需要每個應用都擁有自己獨立的類載入器了,否則無法區分使用的具體是哪一個。
- 類庫共用:每個web應用在tomcat中都可以使用自己版本的jar。但存在如Servlet-api.jar,java原生的包和自定義添加的Java類庫可以相互共用。
- 加強類:類載入器可以在 loadClass 時對 class 進行重寫和覆蓋,在此期間就可以對類進行功能性的增強。比如使用javassist對class進行功能添加和修改,或者添加面向切麵編程時用到的動態代理,以及 debug 等原理。
- 熱替換:在應用正在運行的時候升級軟體,不需要重新啟動應用。比如toccat伺服器中JSP更新替換。
三、自定義類載入器
3.1 ClassLoader實現自定義類載入器相關方法說明
要實現自定義類載入器需要先繼承ClassLoader,ClassLoader類是一個抽象類,負責載入classes的對象。自定義ClassLoader中至少需要瞭解其中的三個的方法: loadClass,findClass,defineClass。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }
loadClass:JVM在載入類的時候,都是通過ClassLoader的loadClass()方法來載入class的,loadClass使用雙親委派模式。如果要改變雙親委派模式,可以修改loadClass來改變class的載入方式。雙親委派模式這裡就不贅述了。
findClass:ClassLoader通過findClass()方法來載入類。自定義類載入器實現這個方法來載入需要的類,比如指定路徑下的文件,位元組流等。
definedClass:definedClass在findClass中使用,通過調用傳進去一個Class文件的位元組數組,就可以方法區生成一個Class對象,也就是findClass實現了類載入的功能了。
貼上一段ClassLoader中loadClass源碼,見見真面目...
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; } }
源碼說明...
/** * Loads the class with the specified <a href="#name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * <tt>resolve</tt> flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting <tt>Class</tt> object. * * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * <p> Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method * during the entire class loading process. * * @param name * The <a href="#name">binary name</a> of the class * * @param resolve * If <tt>true</tt> then resolve the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found */
翻譯過來大概是:使用指定的二進位名稱來載入類,這個方法的預設實現按照以下順序查找類: 調用findLoadedClass(String)方法檢查這個類是否被載入過 使用父載入器調用loadClass(String)方法,如果父載入器為Null,類載入器裝載虛擬機內置的載入器調用findClass(String)方法裝載類, 如果,按照以上的步驟成功的找到對應的類,並且該方法接收的resolve參數的值為true,那麼就調用resolveClass(Class)方法來處理類。 ClassLoader的子類最好覆蓋findClass(String)而不是這個方法。 除非被重寫,這個方法預設在整個裝載過程中都是同步的(線程安全的)。
resolveClass:Class載入必須鏈接(link),鏈接指的是把單一的Class加入到有繼承關係的類樹中。這個方法給Classloader用來鏈接一個類,如果這個類已經被鏈接過了,那麼這個方法只做一個簡單的返回。否則,這個類將被按照 Java™規範中的Execution描述進行鏈接。
3.2 自定義類載入器實現
按照3.1的說明,繼承ClassLoader後重寫了findClass方法載入指定路徑上的class。先貼上自定義類載入器。
package com.chenerzhu.learning.classloader; import java.nio.file.Files; import java.nio.file.Paths; /** * @author chenerzhu * @create 2018-10-04 10:47 **/ public class MyClassLoader extends ClassLoader { private String path; public MyClassLoader(String path) { this.path = path; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClass(name); if (result == null) { throw new ClassNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (Exception e) { e.printStackTrace(); } return null; } private byte[] getClass(String name) { try { return Files.readAllBytes(Paths.get(path)); } catch (Exception e) { e.printStackTrace(); } return null; } }
以上就是自定義的類載入器了,實現的功能是載入指定路徑的class。再看看如何使用。
package com.chenerzhu.learning.classloader; import org.junit.Test; /** * Created by chenerzhu on 2018/10/4. */ public class MyClassLoaderTest { @Test public void testClassLoader() throws Exception { MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class"); Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello"); Object obj = clazz.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } }
首先通過構造方法創建MyClassLoader對象myClassLoader,指定載入src/test/resources/bean/Hello.class路徑的Hello.class(當然這裡只是個例子,直接指定一個class的路徑了)。然後通過myClassLoader方法loadClass載入Hello的Class對象,最後實例化對象。以下是輸出結果,看得出來實例化成功了,並且類載入器使用的是MyClassLoader。
com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca
四、類Class卸載
JVM中class和Meta信息存放在PermGen space區域(JDK1.8之後存放在MateSpace中)。如果載入的class文件很多,那麼可能導致元數據空間溢出。引起java.lang.OutOfMemory異常。對於有些Class我們可能只需要使用一次,就不再需要了,也可能我們修改了class文件,我們需要重新載入 newclass,那麼oldclass就不再需要了。所以需要在JVM中卸載(unload)類Class。
JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載(unload):
- 該類所有的實例都已經被GC。
- 該類的java.lang.Class對象沒有在任何地方被引用。
- 載入該類的ClassLoader實例已經被GC。
很容易理解,就是要被卸載的類的ClassLoader實例已經被GC並且本身不存在任何相關的引用就可以被卸載了,也就是JVM清除了類在方法區內的二進位數據。
JVM自帶的類載入器所載入的類,在虛擬機的生命周期中,會始終引用這些類載入器,而這些類載入器則會始終引用它們所載入的類的Class對象。因此這些Class對象始終是可觸及的,不會被卸載。而用戶自定義的類載入器載入的類是可以被卸載的。雖然滿足以上三個條件Class可以被卸載,但是GC的時機我們是不可控的,那麼同樣的我們對於Class的卸載也是不可控的。
五、JVM自定義類載入器載入指定classPath下的所有class及jar
經過以上幾個點的說明,現在可以實現JVM自定義類載入器載入指定classPath下的所有class及jar了。這裡沒有限制class和jar的位置,只要是classPath路徑下的都會被載入進JVM,而一些web應用伺服器載入是有限定的,比如tomcat載入的是每個應用classPath+“/classes”載入class,classPath+“/lib”載入jar。以下就是代碼啦...
package com.chenerzhu.learning.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Enumeration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * @author chenerzhu * @create 2018-10-04 12:24 **/ public class ClassPathClassLoader extends ClassLoader{ private static Map<String, byte[]> classMap = new ConcurrentHashMap<>(); private String classPath; public ClassPathClassLoader() { } public ClassPathClassLoader(String classPath) { if (classPath.endsWith(File.separator)) { this.classPath = classPath; } else { this.classPath = classPath + File.separator; } preReadClassFile(); preReadJarFile(); } public static boolean addClass(String className, byte[] byteCode) { if (!classMap.containsKey(className)) { classMap.put(className, byteCode); return true; } return false; } /** * 這裡僅僅卸載了myclassLoader的classMap中的class,虛擬機中的 * Class的卸載是不可控的 * 自定義類的卸載需要MyClassLoader不存在引用等條件 * @param className * @return */ public static boolean unloadClass(String className) { if (classMap.containsKey(className)) { classMap.remove(className); return true; } return false; } /** * 遵守雙親委托規則 */ @Override protected Class<?> findClass(String name) { try { byte[] result = getClass(name); if (result == null) { throw new ClassNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (Exception e) { e.printStackTrace(); } return null; } private byte[] getClass(String className) { if (classMap.containsKey(className)) { return classMap.get(className); } else { return null; } } private void preReadClassFile() { File[] files = new File(classPath).listFiles(); if (files != null) { for (File file : files) { scanClassFile(file); } } } private void scanClassFile(File file) { if (file.exists()) { if (file.isFile() && file.getName().endsWith(".class")) { try { byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath())); String className = file.getAbsolutePath().replace(classPath, "") .replace(File.separator, ".") .replace(".class", ""); addClass(className, byteCode); } catch (IOException e) { e.printStackTrace(); } } else if (file.isDirectory()) { for (File f : file.listFiles()) { scanClassFile(f); } } } } private void preReadJarFile() { File[] files = new File(classPath).listFiles(); if (files != null) { for (File file : files) { scanJarFile(file); } } } private void readJAR(JarFile jar) throws IOException { Enumeration<JarEntry> en = jar.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); je.getName(); String name = je.getName(); if (name.endsWith(".class")) { //String className = name.replace(File.separator, ".").replace(".class", ""); String className = name.replace("\\", ".") .replace("/", ".") .replace(".class", ""); InputStream input = null; ByteArrayOutputStream baos = null; try { input = jar.getInputStream(je); baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = input.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } addClass(className, baos.toByteArray()); } catch (Exception e) { e.printStackTrace(); } finally { if (baos != null) { baos.close(); } if (input != null) { input.close(); } } } } } private void scanJarFile(File file) { if (file.exists()) { if (file.isFile() && file.getName().endsWith(".jar")) { try { readJAR(new JarFile(file)); } catch (IOException e) { e.printStackTrace(); } } else if (file.isDirectory()) { for (File f : file.listFiles()) { scanJarFile(f); } } } } public void addJar(String jarPath) throws IOException { File file = new File(jarPath); if (file.exists()) { JarFile jar = new JarFile(file); readJAR(jar); } } }
如何使用的代碼就不貼了,和3.2節自定義類載入器的使用方式一樣。只是構造方法的參數變成classPath了,篇末有代碼。當創建MyClassLoader對象時,會自動添加指定classPath下麵的所有class和jar裡面的class到classMap中,classMap維護className和classCode位元組碼的關係,只是個緩衝作用,避免每次都從文件中讀取。自定義類載入器每次loadClass都會首先在JVM中找是否已經載入className的類,如果不存在就會到classMap中取,如果取不到就是載入錯誤了。
六、最後
至此,JVM自定義類載入器載入指定classPath下的所有class及jar已經完成了。這篇博文花了兩天才寫完,在寫的過程中有意識地去瞭解了許多代碼的細節,收穫也很多。本來最近僅僅是想實現Quartz控制台頁面任務添加支持動態class,結果不知不覺跑到類載入器的坑了,在此也趁這個機會總結一遍。當然以上內容並不能保證正確,所以希望大家看到錯誤能夠指出,幫助我更正已有的認知,共同進步。。。
本文的代碼已經上傳github:https://github.com/chenerzhu/learning/tree/master/classloader 歡迎下載和指正。
參考文章: