一、前言
《Java魔法堂:類加載機制入了個門》中提及整個類加載流程中只有加載階段作為碼農的我們可以入手干預,其余均由JVM處理。本文將記錄加載階段的核心組件——類加載器的相關信息,以便日后查閱。若有紕漏請大家指正,謝謝。
注意:以下內容基于JDK7和HotSpot VM。
二、類加載器種類及其關系
從上圖可知Java主要有4種類加載器
1. Bootstrap ClassLoader(引導類加載器):作為JVM的一部分無法在應用程序中直接引用,由C/C++實現(其他JVM可能通過Java來實現)。負責加載①<JAVA>/jre/lib目錄 或 ②-Xbootclasspath參數所指定的目錄 或 ③系統屬性sun.boot.class.path指定的目錄 中特定名稱的jar包。在JVM啟動時將通過Bootstrap ClassLoader加載rt.jar,并初始化sun.misc.Launcher從而創建Extension ClassLoader和System ClassLoader實例,和將System ClassLoader實例設置為主線程的默認Context ClassLoader(線程上下文加載器)。
注意:Bootstrap ClassLoader只會加載特定名稱的類庫,如rt.jar等。假如我們自己定義一個jar類庫丟進<JAVA_HOME>/jre/lib目錄下也不會被加載的!
下面我們看看Bootstrap ClassLoader到底加載了哪些jar包吧!
import java.net.*; import sun.misc.*; class Main{ public static void main(String[] args){ URL[] urls = Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) System.out.println(url.toExternalForm()); } } /* vim:!javac % & java Main 后輸出 * lib/resources.jar * lib/rt.jar * lib/sunrsasign.jar * lib/jsse.jar * lib/jce.jar * lib/charsets.jar * lib/jfr.jar * lib/classe */
2. Extension ClassLoader(擴展類加載器):僅含一個實例,由 sun.misc.Launcher$ExtClassLoader 實現,負責加載①<JAVA_HOME>/jre/lib/ext目錄 或 ②系統屬性java.ext.dirs所指定的目錄 中的所有類庫。
3. App/System ClassLoader(系統類加載器):僅含一個實例,由 sun.misc.Launcher$AppClassLoader 實現,可通過 java.lang.ClassLoader.getSystemClassLoader 獲取。負責加載 ①系統環境變量ClassPath 或 ②-cp 或 ③系統屬性java.class.path 所指定的目錄下的類庫。
4. Custom ClassLoader(用戶自定義類加載器):可同時存在多個用戶自定義的類加載器,具體如何定義請參考后文。
除了上面的4種類加載器外,JDK1.2開始引入了另一個類加載器——Context ClassLoader(線程上下文加載器)。
5. Context ClassLoader(線程上下文加載器):默認為System ClassLoader,可通過Thread.currentThread().setContextClassLoader(ClassLoader)來設置,可通過ClassLoader Thread.currentThread().getContextClassLoader()來獲取。每個線程均將Context ClassLoader預先設置為父線程的Context ClassLoader。該類加載器主要用于打破雙親委派模型,容許父類加載器通過子類加載器加載所需的類庫。
三、雙親委派模型
在介紹雙親委派模型前先看看以下示例:
/* * Main.java文件 */ import java.net.*; import java.lang.reflect.*; class Main{ public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException{ ClassLoader pClassLoader = ClassLoader.getSystemClassLoader(); // 以System ClassLoader作為父類加載器 URL[] baseUrls = {new URL("file:/d:/testLib/")}; // 搜索類庫的目錄 final String binaryName = "com.fsjohnhuang.HelloWorld"; // 需要加載的類的二進制名稱 ClassLoader userClassLoader1 = new URLClassLoader(baseUrls, pClassLoader); ClassLoader userClassLoader2 = new URLClassLoader(baseUrls, pClassLoader); Class clazz1 = userClassLoader1.loadClass(binaryName); Class clazz2 = userClassLoader2.loadClass(binaryName); Object instance1 = clazz1.newInstance(); Object instance2 = clazz2.newInstance(); // 調用say方法 clazz1.getMethod("say").invoke(instance1); clazz2.getMethod("say").invoke(instance2); // 輸出類的二進制名稱 System.out.println(clazz1.toString()); System.out.println(clazz2.toString()); // 比較兩個類的地址是否相同 System.out.println(clazz1 == clazz2); // 比較兩個類是否相同或是否為繼承關系 System.out.println(clazz1.isAssignableFrom(clazz2)); // 查看類型轉換是否成功 boolean ret = true; try{ clazz2.cast(instance1); } catch(ClassCastException e){ ret = false; } System.out.println(ret); } }
結果:
Hello World! Hello World! class com.fsjohnhuang.HelloWorld class com.fsjohnhuang.HelloWorld false false false
奇了個怪了,為什么兩個類的Class實例不一樣呢?這是因為 對于任意一個類,都需要由加載它的類加載器和該類本身一同確立其在JVM中的唯一性。也就是說對于同一個類文件,通過不同的類加載器加載那么在JVM中就生成了不同的類。
那現在問題來了,我們知道由java.lang.*(打包到rt.jar中)是由Bootstrap ClassLoader加載的,現在我閑著蛋疼自定義一個類加載器來加載java.lang.String,按照上面的定義那JVM中就有兩個java.lang.String類了,然后出現下列問題:
if (myString.newInstance() instanceof String){ System.out.println("1"); // 絕對不會執行這一句 } else{ System.out.println("2"); }
注意:由于類會通過自身對應的類加載器加載其引用的其他類。若myString中還引用了其他類,那么將會通過我自定的類加載器來加載一次哦!
假如會發生上述情況,真實項目中發生的問題就更大了。(注意:上述代碼在真實環境絕對無法成立,自定義的類加載器本身就被限制為無法加載java.*的類哦!)
雙親委派模型就是用于解決上述問題,越基礎的類由越上層的類加載器進行加載,如Java API類庫則有Bootstrap ClassLoader加載。具體如下:
當一個類加載器收到類加載的請求,首先會將請求委派給父類加載器,這樣一層一層委派到Bootstrap ClassLoader。然后加載器根據請求嘗試搜索和加載類,若搜索失敗則向子類加載器反饋信息(拋出ClassNotFoundException),然后子類加載器才嘗試自己去加載。JAVA中采用組合的方式實現雙親委派模型,而不是繼承的方式。
不難發現Bootstrap、Extension和System三種類加載器默認的加載類的目錄路徑均是不同的,也可以說 類的來源地與類加載器應該是一一對應。位于同一來源地的類應該由相同的類加載器加載,而不是由其他類加載來加載,或者通過雙親委派模型將加載請求傳遞給相應的類加載器。因最基礎的類庫通過Bootstrap加載,其次則由Extension加載,應用程序的則由System來加載,應用程序動態依賴的功能模塊則通過用戶自定義類加載器加載。
四、非雙親委派模型
雙親委派模型解決了類重復加載的亂象。但現在問題又來了,雙親委派模型僅限于子類加載器將加載請求轉發到父類加載器,請求是單向流動的,那如果通過父類加載器加載一個在子類加載器管轄類來源的類,那怎么辦呢?再說真的有這樣的場景嗎?
首先我們將 “通過父類加載器加載一個在子類加載器管轄類來源的類” 具體化為 “在一個由Bootstrap ClassLoader加載的類中動態加載其他目錄路徑下的類庫”,這樣我們就輕松地找到JNDI、JAXP等SPI(Service Provider Interface)均符合這種應用場景。以下就以JAXP來介紹吧!
JAXP(Java API for XML Processing),用于處理XML文檔的API,接口和默認實現位于rt.jar中,但增強型的具體實現則由各個廠家提供且以第三方jar包的形式部署在項目的CLASSPATH下。其中抽象類 javax.xml.parsers.DocumentBuilderFactory的類方法newInstance(String factoryClassName, ClassLoader classLoader) 可根據二進制名稱獲取由各廠家具體實現的DocumentBuilderFactory實例。現在以 javax.xml.parsers.DocumentBuilderFactory.newInstance(" org.apache.xerces.jaxp.DocumentBuilderFactoryImpl", null) 的調用形式來深入下去。
首先假設newInstance內部是以以下方式加載類的
Class.forName("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl"); // 或 this.getClass().getClassLoader.loadClass("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
由于DocumentBuilderFactory是由Boostrap ClassLoader加載的,因此上述操作結果是通過Bootstrap ClassLoader來加載第三方類庫,結果必須是ClassNotFoundException的。也就是說我們需要獲取System ClassLoader或它的子類加載器才能成功加載這個類。
首先想到的是通過ClassLoader.getSystemClassLoader()方法來獲取System ClassLoader。然而JDK1.2又引入了另一個更靈活的方式,那就是Context ClassLoader(線程上下文類加載器,默認為System ClassLoader),通過Context ClassLoader我們可以獲取System ClassLoader或它的子類加載器,從而可以加載CLASSPATH和其他路徑下的類庫。
newInstance(String, ClassLoader)的實際實現是調用FactoryFinder.newInstance方法,而該方法則調用getProviderClass方法來獲取Class實例,getProviderClass方法中則通過SecuritySupport的實例方法getContextClassLoader()來獲取類加載器,代碼片段如下:
ClassLoader getContextClassLoader() throws SecurityException { return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Object run() { ClassLoader cl = null; cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { cl = ClassLoader.getSystemClassLoader(); } return cl; } }); }
注意:Context ClassLoader可是要慎用哦!因為可以通過setContextClassLoader方法動態設置線程上下文類加載器,也就是有可能每次調用時的類加載器均不相同(所管轄的目錄路徑也不相同),在并發環境下就更容易出問題了。
五、從源碼理解
首先我們看看ExtClassLoader和AppClassLoader是如何創建的,目光移到sun/misc/Launcher.java文件中,而ExtClassLoader和AppClassLoader則以Luancher的內部類的形式實現。在Launcher類進入初始化階段時會創建一個Launcher實例,其構造函數中會實例化ExtClassLoader,然后以ExtClassLoader實例作為父類加載器來實例化AppClassLoader,并將AppClassLoader實例設置為主線程默認的Context ClassLoader。
public Launcher() { ExtClassLoader localExtClassLoader; try { // 實例化ExtClassLoader localExtClassLoader = ExtClassLoader.getExtClassLoader(); } catch (IOException localIOException1) { throw new InternalError("Could not create extension class loader"); } try { // 實例化AppClassLoader this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader); } catch (IOException localIOException2) { throw new InternalError("Could not create application class loader"); } // 主線程的默認Context ClassLoader Thread.currentThread().setContextClassLoader(this.loader); String str = System.getProperty("java.security.manager"); if (str != null) { SecurityManager localSecurityManager = null; if (("".equals(str)) || ("default".equals(str))) localSecurityManager = new SecurityManager(); else try { localSecurityManager = (SecurityManager)this.loader.loadClass(str).newInstance(); } catch (IllegalAccessException localIllegalAccessException) { } catch (InstantiationException localInstantiationException) { } catch (ClassNotFoundException localClassNotFoundException) { } catch (ClassCastException localClassCastException) { } if (localSecurityManager != null) System.setSecurityManager(localSecurityManager); else throw new InternalError("Could not create SecurityManager: " + str); } }
ExtClassLoader和AppClassLoader均繼承了java.net.URLClassLoader,并且僅對類的加載、搜索目錄路徑作修改而已。如AppClassLoader的getAppClassLoader方法:
static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader) throws IOException { // 獲取搜索、加載類的目錄路徑 String str = System.getProperty("java.class.path"); final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str); return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Launcher.AppClassLoader run() { URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile); // 設置類加載器的搜索、加載類的目錄路徑,并創建一個類加載器實例 return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader); } }); }
在研究URLClassLoader之前我們先看看java.lang.ClassLoader,除Bootstrap ClassLoader外所有類加載器必須繼承ClassLoader。還記得 ClassLoader.getSystemClassLoader().loadClass("org.apache.xerces.jaxp.DocumentBuilderFactoryImpl") 吧,現在我們就從loadClass出發,看看整個類加載機制吧!
protected Class<?> loadClass(String paramString, boolean paramBoolean) throws ClassNotFoundException { synchronized (getClassLoadingLock(paramString)) { // 檢查該類是否已加載過,若已加載過則返回緩存中的Class實例 Class localClass = findLoadedClass(paramString); // 下面是雙親委派模型的具體實現 if (localClass == null) { long l1 = System.nanoTime(); try { // 若有父類加載器則將加載請求傳遞到父類加載器 // 若parent變量為null則表示父類加載器是Bootstrap ClassLoader,同樣將加載請求傳遞到父類加載器 if (this.parent != null) localClass = this.parent.loadClass(paramString, false); else { localClass = findBootstrapClassOrNull(paramString); } } catch (ClassNotFoundException localClassNotFoundException) { // 父類加載器無法加載給類時則拋出異常 } if (localClass == null) { long l2 = System.nanoTime(); // 開始加載類了! localClass = findClass(paramString); PerfCounter.getParentDelegationTime().addTime(l2 - l1); PerfCounter.getFindClassTime().addElapsedTimeFrom(l2); PerfCounter.getFindClasses().increment(); } } // 對類執行解析操作 if (paramBoolean) { resolveClass(localClass); } return localClass; } }
可以看到loadClass方法內部主要為雙親委派模型的實現,實際的類加載操作是在findClass方法中實現的。另外由于不允許同一個類加載器重復加載同一個類,因此當對同一個類重復進行加載操作時,則通過findLoadedClass方法來返回已有的Class實例。
ClassLoader中指提供findClass的定義,具體實現由子類提供。而URLClassLoader的findClass則是通過URLClassPath實例來獲取類的二進制數據,然后調用defineClass對二進制數據進行初步驗證,然后在由ClassLoader的defineClass進行其余的驗證后生成Class實例返回。
protected Class<?> findClass(final String paramString) throws ClassNotFoundException { try { return (Class)AccessController.doPrivileged(new PrivilegedExceptionAction() { public Class run() throws ClassNotFoundException { // ucp為URLClassPath實例 // 通過URLClassPath實例獲取類的二進制數據 String str = paramString.replace('.', '/').concat(".class"); Resource localResource = URLClassLoader.this.ucp.getResource(str, false); if (localResource != null) { try { // 調用URLClassLoader的defineClass方法驗證類的二進制數據并返回Class實例 return URLClassLoader.this.defineClass(paramString, localResource); } catch (IOException localIOException) { throw new ClassNotFoundException(paramString, localIOException); } } throw new ClassNotFoundException(paramString); } } , this.acc); } catch (PrivilegedActionException localPrivilegedActionException) { throw ((ClassNotFoundException)localPrivilegedActionException.getException()); } }
總結一下, 類加載過程為loadClass -> findClass -> defineClass。loadClass為雙親委派的實現,defineClass為類數據驗證和生成Class實例,findClass為獲取類的二進制數據。
那么我們自定義類加載器時只需重寫findClass就可以加載不同路徑下的類庫了!
六、手動加載類吧,騷年!
手動加載類的形式是多樣的,具體如下:
1. 利用現有的類加載器
// 通過當前類的類加載器加載(會執行初始化) Class.forName("二進制名稱"); Class.forName("二進制名稱", true, this.getClass().getClassLoader()); // 通過當前類的類加載器加載(不會執行初始化) Class.forName("二進制名稱", false, this.getClass().getClassLoader()); this.getClass().loadClass("二進制名稱"); // 通過系統類加載器加載(不會執行初始化) ClassLoader.getSystemClassLoader().loadClass("二進制名稱"); // 通過線程上下文類加載器加載(不會執行初始化) Thread.currentThread().getContextClassLoader().loadClass("二進制名稱");
2. 利用URLClassLoader
URL[] baseUrls = {new URL("file:/d:/testLib/")}; URLClassLoader loader = new URLClassLoader(baseUrl, ClassLoader.getContextClassLoader()); Class clazz = loader.loadClass("com.fsjohnhuang.HelloWorld");
3. 繼承ClassLoader自定義類加載器
public class MyClassLoader extends ClassLoader{ private String dir; public MyClassLoader(String dir, ClassLoader parent){ super(parent); this.dir = dir; } @Override protect Class<?> findClass(String binaryName) throws ClassNotFoundException{ String pathSegmentSeperator = System.getProperty("file.separator"); String path = binaryName.replace(".", pathSegmentSeperator ).concat(".class"); FileInputStream fis = new FileInputStream(dir + pathSegmentSeperator + path); byte[] b = new byte[fis.available()]; fis.read(b, 0, b.length); fis.close(); return defineClass(binaryName, b, 0, b.length); } }
七、如何卸載類?
類卸載實質上就是GC對方法區(HotSpot中可稱為永久代)的類數據進行垃圾回收。
虛擬機規范:
A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result , system classes may never be unloaded.
只有當加載該類型的類加載器實例( 非類加載器類型) 為unreachable 狀態時,當前被加載的類型才被卸載. 啟動類加載器實例永遠為reachable 狀態,由啟動類加載器加載的類型可能永遠不會被卸載。
Unreachable狀態的解釋:
1 、A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
2 、finalizer-reachable: A finalizer-reachable object can be reached from some finalizable object through some chain of references, but not from any live thread. An unreachable object cannot be reached by either means.
也就是說
1. 加載器的類實例已經被回收。
2. 類的實例已經被回收。
3. 類的Class實例沒有被任何地方引用,無法在任何地方通過反射訪問該類。
對于Bootstrap、Ext和Sys類加載器來說正常情況下是不會被回收的,只有用戶自定義類加載器才可以。通過 $ java -verbose:class Main 執行以下代碼。
import java.net.*; import java.io.*; public class Main{ public static class MyURLClassLoader extends URLClassLoader { public MyURLClassLoader() { super (getMyURLs()); } private static URL[] getMyURLs() { try { return new URL[]{ new File ("d:/").toURL()}; } catch (Exception e) { e.printStackTrace(); return null ; } } } public static void main(String[] args) throws IOException{ try { MyURLClassLoader classLoader = new MyURLClassLoader(); Class classLoaded = classLoader.loadClass("RMDIR"); System.out.println(classLoaded.getName()); classLoaded = null ; classLoader = null ; System.out.println(" 開始GC"); System.gc(); System.out.println("GC 完成"); System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }
八、加載圖片、視頻等非類資源
ClassLoader除了用于加載類外,還可以用于加載圖片、視頻等非類資源。同樣是采用雙親委派模型將加載資源的請求傳遞到頂層的Bootstrap ClassLoader,在其管轄的目錄下搜索資源,若失敗才逐層返回逐層搜索。
相關的實例方法如下:
URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)
而相關的類方法均是調用系統類加載器的上述方法而已。
九、總結
若有紕漏請大家指正,謝謝!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4284515.html ^_^肥仔John
十、參考
http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://blog.csdn.net/zhoudaxia/article/details/35897057
《深入理解Java虛擬機 JVM高級特性與最佳實踐》
文章列表