一,、JVM中的類加載器類型 從Java虛擬機(jī)的角度講,只有兩種不同的類加載器:啟動類加載器和其他類加載器,。 1.啟動類加載器(Boostrap ClassLoader):這個是由c 實(shí)現(xiàn)的,,主要負(fù)責(zé)JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項(xiàng)指定的jar包裝入工作。 2.其他類加載器:由java實(shí)現(xiàn),可以在方法區(qū)找到其Class對象,。這里又細(xì)分為幾個加載器 a).擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)用于加載JAVA_HOME/lib/ext目錄中的,,或者被-Djava.ext.dirs系統(tǒng)變量指定所指定的路徑中所有類庫(jar),開發(fā)者可以直接使用擴(kuò)展類加載器,。java.ext.dirs系統(tǒng)變量所指定的路徑的可以通過System.getProperty("java.ext.dirs")來查看,。 b).應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)java -classpath或-Djava.class.path所指的目錄下的類與jar包裝入工作。開發(fā)者可以直接使用這個類加載器,。在沒有指定自定義類加載器的情況下,,這就是程序的默認(rèn)加載器。 c).自定義類加載器(User ClassLoader):在程序運(yùn)行期間, 通過java.lang.ClassLoader的子類動態(tài)加載class文件, 體現(xiàn)java動態(tài)實(shí)時類裝入特性,。
這四個類加載器的層級關(guān)系,,如下圖所示。
二,、為什么要自定義類加載器 區(qū)分同名的類:假定在tomcat 應(yīng)用服務(wù)器,,上面部署著許多獨(dú)立的應(yīng)用,同時他們擁有許多同名卻不同版本的類,。要區(qū)分不同版本的類當(dāng)然是需要每個應(yīng)用都擁有自己獨(dú)立的類加載器了,,否則無法區(qū)分使用的具體是哪一個。 類庫共享:每個web應(yīng)用在tomcat中都可以使用自己版本的jar,。但存在如Servlet-api.jar,,java原生的包和自定義添加的Java類庫可以相互共享。 加強(qiáng)類:類加載器可以在 loadClass 時對 class 進(jìn)行重寫和覆蓋,,在此期間就可以對類進(jìn)行功能性的增強(qiáng),。比如使用javassist對class進(jìn)行功能添加和修改,或者添加面向切面編程時用到的動態(tài)代理,,以及 debug 等原理,。 熱替換:在應(yīng)用正在運(yùn)行的時候升級軟件,不需要重新啟動應(yīng)用,。比如toccat服務(wù)器中JSP更新替換,。
三、自定義類加載器 3.1 ClassLoader實(shí)現(xiàn)自定義類加載器相關(guān)方法說明 要實(shí)現(xiàn)自定義類加載器需要先繼承ClassLoader,,ClassLoader類是一個抽象類,,負(fù)責(zé)加載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()方法來加載類,。自定義類加載器實(shí)現(xiàn)這個方法來加載需要的類,,比如指定路徑下的文件,字節(jié)流等,。 definedClass:definedClass在findClass中使用,,通過調(diào)用傳進(jìn)去一個Class文件的字節(jié)數(shù)組,就可以方法區(qū)生成一個Class對象,,也就是findClass實(shí)現(xiàn)了類加載的功能了,。
貼上一段ClassLoader中l(wèi)oadClass源碼,見見真面目...
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
*/
翻譯過來大概是:使用指定的二進(jìn)制名稱來加載類,,這個方法的默認(rèn)實(shí)現(xiàn)按照以下順序查找類: 調(diào)用findLoadedClass(String)方法檢查這個類是否被加載過 使用父加載器調(diào)用loadClass(String)方法,,如果父加載器為Null,類加載器裝載虛擬機(jī)內(nèi)置的加載器調(diào)用findClass(String)方法裝載類,, 如果,,按照以上的步驟成功的找到對應(yīng)的類,并且該方法接收的resolve參數(shù)的值為true,那么就調(diào)用resolveClass(Class)方法來處理類,。 ClassLoader的子類最好覆蓋findClass(String)而不是這個方法,。 除非被重寫,這個方法默認(rèn)在整個裝載過程中都是同步的(線程安全的),。
resolveClass:Class載入必須鏈接(link),鏈接指的是把單一的Class加入到有繼承關(guān)系的類樹中,。這個方法給Classloader用來鏈接一個類,,如果這個類已經(jīng)被鏈接過了,那么這個方法只做一個簡單的返回,。否則,,這個類將被按照 Java?規(guī)范中的Execution描述進(jìn)行鏈接。
3.2 自定義類加載器實(shí)現(xiàn) 按照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; } }
以上就是自定義的類加載器了,,實(shí)現(xiàn)的功能是加載指定路徑的class,。再看看如何使用,?!?/p>
package com.chenerzhu.learning.classloader;
import org.junit.Test;
/**
首先通過構(gòu)造方法創(chuàng)建MyClassLoader對象myClassLoader,指定加載src/test/resources/bean/Hello.class路徑的Hello.class(當(dāng)然這里只是個例子,直接指定一個class的路徑了)。然后通過myClassLoader方法loadClass加載Hello的Class對象,最后實(shí)例化對象,。以下是輸出結(jié)果,看得出來實(shí)例化成功了,,并且類加載器使用的是MyClassLoader,。
com.chenerzhu.learning.classloader.bean.Hello@2b2948e2 com.chenerzhu.learning.classloader.MyClassLoader@335eadca
四,、類Class卸載 JVM中class和Meta信息存放在PermGen space區(qū)域(JDK1.8之后存放在MateSpace中),。如果加載的class文件很多,那么可能導(dǎo)致元數(shù)據(jù)空間溢出。引起java.lang.OutOfMemory異常,。對于有些Class我們可能只需要使用一次,,就不再需要了,,也可能我們修改了class文件,,我們需要重新加載 newclass,那么oldclass就不再需要了,。所以需要在JVM中卸載(unload)類Class,。 JVM中的Class只有滿足以下三個條件,才能被GC回收,,也就是該Class被卸載(unload):
該類所有的實(shí)例都已經(jīng)被GC,。 該類的java.lang.Class對象沒有在任何地方被引用,。 加載該類的ClassLoader實(shí)例已經(jīng)被GC。 很容易理解,,就是要被卸載的類的ClassLoader實(shí)例已經(jīng)被GC并且本身不存在任何相關(guān)的引用就可以被卸載了,,也就是JVM清除了類在方法區(qū)內(nèi)的二進(jìn)制數(shù)據(jù)。 JVM自帶的類加載器所加載的類,,在虛擬機(jī)的生命周期中,,會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,。因此這些Class對象始終是可觸及的,,不會被卸載。而用戶自定義的類加載器加載的類是可以被卸載的,。雖然滿足以上三個條件Class可以被卸載,,但是GC的時機(jī)我們是不可控的,那么同樣的我們對于Class的卸載也是不可控的,。
五,、JVM自定義類加載器加載指定classPath下的所有class及jar 經(jīng)過以上幾個點(diǎn)的說明,現(xiàn)在可以實(shí)現(xiàn)JVM自定義類加載器加載指定classPath下的所有class及jar了,。這里沒有限制class和jar的位置,,只要是classPath路徑下的都會被加載進(jìn)JVM,而一些web應(yīng)用服務(wù)器加載是有限定的,,比如tomcat加載的是每個應(yīng)用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,虛擬機(jī)中的
- Class的卸載是不可控的
- 自定義類的卸載需要MyClassLoader不存在引用等條件
- @param className
- @return
*/ public static boolean unloadClass(String className) { if (classMap.containsKey(className)) { classMap.remove(className); return true; } return false; }
/**
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節(jié)自定義類加載器的使用方式一樣,。只是構(gòu)造方法的參數(shù)變成classPath了,篇末有代碼。當(dāng)創(chuàng)建MyClassLoader對象時,,會自動添加指定classPath下面的所有class和jar里面的class到classMap中,,classMap維護(hù)className和classCode字節(jié)碼的關(guān)系,只是個緩沖作用,,避免每次都從文件中讀取,。自定義類加載器每次loadClass都會首先在JVM中找是否已經(jīng)加載className的類,如果不存在就會到classMap中取,,如果取不到就是加載錯誤了,。
六、最后 至此,,JVM自定義類加載器加載指定classPath下的所有class及jar已經(jīng)完成了,。這篇博文花了兩天才寫完,在寫的過程中有意識地去了解了許多代碼的細(xì)節(jié),,收獲也很多,。本來最近僅僅是想實(shí)現(xiàn)Quartz控制臺頁面任務(wù)添加支持動態(tài)class,結(jié)果不知不覺跑到類加載器的坑了,,在此也趁這個機(jī)會總結(jié)一遍,。當(dāng)然以上內(nèi)容并不能保證正確,所以希望大家看到錯誤能夠指出,,幫助我更正已有的認(rèn)知,,共同進(jìn)步。,。,。
|