這篇文章帶給大家的內容是關於JVM自訂類別載入器如何載入指定classPath下的所有class和jar,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
從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更新替換
要實作自訂類別載入器需要先繼承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.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
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了。这里没有限制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
以上是JVM自訂類別載入器如何載入指定classPath下的所有class和jar的詳細內容。更多資訊請關注PHP中文網其他相關文章!