ClassLoader背景知識

前幾天想了一下,最近主要學習linux和httpd,所以tomcat源碼閱讀先放一放,可能到9月份左右再繼續。不過先把已經寫好的幾篇陸續貼上來 

tomcat用到很多ClassLoader相關的代碼,如果缺乏這方面的背景知識,閱讀源碼會遇到很多障礙,所以本文首先總結一下這方面的內容,和tomcat源碼的關係不大 

1 標準的ClassLoader體系 

 

1.1 bootstrap 

bootstrap classloader是由JVM啓動的,用於加載%JAVA_HOME%/jre/lib/下的JAVA平臺自身的類(比如rt.jar中的類等)。這個classloader位於JAVA類加載器鏈的頂端,是用C/C++開發的,而且JAVA應用中沒有任何途徑可以獲取到這個實例,它是JDK實現的一部分 

1.2 extension 

entension classloader用於加載%JAVA_HOME%/jre/lib/ext/下的類,它的實現類是sun.misc.Launcher$ExtClassLoader,是一個內部類 

基本上,我們開發的JAVA應用都不太需要關注這個類 

1.3 system 

system classloader是jvm啓動時,根據classpath參數創建的類加載器(如果沒有顯式指定classpath,則以當前目錄作爲classpath)。在普通的JAVA應用中,它是最重要的類加載器,因爲我們寫的所有類,通常都是由它加載的。這個類加載器的實現類是sun.misc.Launch$AppClassLoader 

用ClassLoader.getSystemClassLoader(),可以得到這個類加載器 

1.4 custom 

一般情況下,對於普通的JAVA應用,ClassLoader體系就到system爲止了。平時編程時,甚至都不會感受到classloader的存在 

但是對於其他一些應用,比如web server,插件加載器等,就必須和ClassLoader打交道了。這時候默認的類加載器不能滿足需求了(類隔離、運行時加載等需求),需要自定義類加載器,並掛載到ClassLoader鏈中(默認會掛載到system classloader下面) 

2 雙親委派模型 

從上面的圖可以看到,classloader鏈,是一個自上而下的樹形結構。一般來說,java中的類加載,是遵循雙親委派模型的,即: 

當一個classloader要加載一個類時,首先會委託給它的parent classloader來加載,如果parent找不到,纔會自己加載。如果最後也找不到,則會拋出熟悉的ClassNotFoundException 

這個模型,是在最基礎的抽象類ClassLoader裏確定的: 
[java] view plain copy
  1. protected synchronized Class<?> loadClass(String name, boolean resolve)  
  2.     throws ClassNotFoundException  
  3.     {  
  4.     // First, check if the class has already been loaded  
  5.     Class c = findLoadedClass(name);  
  6.     if (c == null) {  
  7.         try {  
  8.         if (parent != null) {  
  9.             c = parent.loadClass(name, false);  
  10.         } else {  
  11.             c = findBootstrapClassOrNull(name);  
  12.         }  
  13.         } catch (ClassNotFoundException e) {  
  14.                 // ClassNotFoundException thrown if class not found  
  15.                 // from the non-null parent class loader  
  16.             }  
  17.             if (c == null) {  
  18.             // If still not found, then invoke findClass in order  
  19.             // to find the class.  
  20.             c = findClass(name);  
  21.         }  
  22.     }  
  23.     if (resolve) {  
  24.         resolveClass(c);  
  25.     }  
  26.     return c;  
  27.     }  

自定義ClassLoader的時候,一般來說,需要做的並不是覆蓋loadClass()方法,這樣的話就“破壞”了雙親委派模型;需要做的只是實現findClass()方法即可 

不過,從上面的代碼也可以看出,雙親委派模型只是一種“建議”,並沒有強制保障的措施。如果自定義的ClassLoader無視此規定,直接自行加載,不將請求委託給parent,當然也是沒問題的 

在實際情況中,雙親委派模型被“破壞”也是很常見的。比如在tomcat裏,webappx classloader就不會委託給上層的common classloader,而是先委託給system,然後自己加載,最後才委託給common;再比如說在OSGi裏,更是有意完全打破了這個規則 

當然,對於普通的JAVA應用開發來說,需要自定義classloader的場景本來就不多,需要去違反雙親委派模型的場景,更是少之又少 

3 自定義ClassLoader 

3.1 自定義ClassLoader的一般做法 

從上面的代碼可以看到,自定義ClassLoader很簡單,只要繼承抽象類ClassLoader,再實現findClass()方法就可以了 

3.2 自定義ClassLoader的場景 

事實上,需要實現新的ClassLoader的場景是很少的 

注意:需要增加一個自定義ClassLoader的場景很多;但是,需要自己實現一個新的ClassLoader子類的場景不多。這是兩回事,不可混淆 

比如,即使在tomcat裏,也沒有自行實現新的ClassLoader子類,只是創建了URLClassLoader的實例,作爲custom classloader 

3.3 ClassLoader的子類 

在JDK中已經提供了若干ClassLoader的子類,在需要的時候,可以直接創建實例並使用。其中最常用的是URLClassLoader,用於讀取一個URL下的資源,從中加載Class 
[java] view plain copy
  1. @Deprecated  
  2. public class StandardClassLoader  
  3.     extends URLClassLoader  
  4.     implements StandardClassLoaderMBean {  
  5.   
  6.     public StandardClassLoader(URL repositories[]) {  
  7.         super(repositories);  
  8.     }  
  9.   
  10.     public StandardClassLoader(URL repositories[], ClassLoader parent) {  
  11.         super(repositories, parent);  
  12.     }  
  13.   
  14. }  

可以看到,tomcat就是在URLClassLoader的基礎上,包裝了StandardClassLoader,實際上並沒有任何功能上的區別 

3.4 設置parent 

在抽象類ClassLoader中定義了一個parent字段,保存的是父加載器。但是這個字段是private的,並且沒有setter方法 

這就意味着只能在構造方法中,一次性地設置parent classloader。如果沒有設置的話,則會默認將system classloader設置爲parent,這也是在ClassLoader類中確定的: 
[java] view plain copy
  1. protected ClassLoader(ClassLoader parent) {  
  2.         this(checkCreateClassLoader(), parent);  
  3.     }  
  4.   
  5. protected ClassLoader() {  
  6.         this(checkCreateClassLoader(), getSystemClassLoader());  
  7.     }  


4 ClassLoader隱性傳遞 

“隱性傳遞”這個詞是我亂造的,在網上和註釋中沒有找到合適的描述的詞 

試想這樣一種場景:在應用中需要加載100個類,其中70個在classpath下,默認由system來加載,這部分不需要額外處理;另外30個類,由自定義classloader加載,比如在tomcat裏: 
[java] view plain copy
  1. Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");  
  2. Object startupInstance = startupClass.newInstance();  

如上,org.apache.catalina.startup.Catalina是由自定義類加載器加載的,需要額外編程來處理(如果是system加載的,直接new就可以了) 

如果30個類,都要通過這種方式來加載,就太麻煩了。不過classloader有一個特性,就是“隱性傳遞”,即: 

如果一個ClassA是由某個ClassLoader加載的,那麼ClassA中依賴的需要加載的類,默認也會由同一個ClassLoader加載 

這個機制是由JVM保證的,對於程序員來說是透明的 

5 current classloader 

5.1 定義 

與前面說的extension、system等不同,在運行時並不存在一個實際的“current classloader”,只是一個抽象的概念。指的是一個類“當前的”類加載器。一個對象實例所屬的Class,是由哪一個ClassLoader加載的,這個ClassLoader就是這個對象實例的current classloader 

獲得的方法是: 
[java] view plain copy
  1. this.getClass().getClassLoader();  


5.2 實例 

current classloader概念的意義,主要在於它會影響Class.forName()方法的表現,貼一段代碼進行說明: 
[java] view plain copy
  1. public class Test {  
  2.   
  3.     public void tryForName() {  
  4.   
  5.         System.out.println("current classloader: "  
  6.                 + this.getClass().getClassLoader());  
  7.   
  8.         try {  
  9.             Class.forName("net.kyfxbl.test.cl.Target");  
  10.                         System.out.println("load class success");  
  11.         } catch (ClassNotFoundException e) {  
  12.             e.printStackTrace();  
  13.         }  
  14.   
  15.     }  
  16.   
  17. }  

這個類調用了Class.forName()方法,試圖加載net.kyfxbl.test.cl.Target類(Target是一個空類,作爲加載目標,不重要)。這個類在運行時能否加載Target成功,取決於它的current classloader,能不能加載到Target 

首先,將Test和Target打成jar包,放到classpath之外,jar包中內容如下: 



然後在工程中刪除Target類(classpath中加載不到Target了) 

在Main中用system 加載Test,此時Test的current classloader是system,加載Target類失敗 
[java] view plain copy
  1. public static void main(String[] args) {  
  2.   
  3.         Test t = new Test();  
  4.         t.tryForName();  
  5.   
  6.     }  

然後,這次用自定義的classloader來加載 
[java] view plain copy
  1. public static void main(String[] args) throws Exception {  
  2.   
  3.         ClassLoader cl = createClassLoader();  
  4.   
  5.         Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");  
  6.         Object startupInstance = startupClass.newInstance();  
  7.   
  8.         String methodName = "tryForName";  
  9.         Class<?>[] paramTypes = new Class[0];  
  10.         Object[] paramValues = new Object[0];  
  11.         Method method = startupInstance.getClass().getMethod(methodName,  
  12.                 paramTypes);  
  13.         method.invoke(startupInstance, paramValues);  
  14.   
  15.     }  
  16.   
  17.     private static ClassLoader createClassLoader() throws MalformedURLException {  
  18.   
  19.         String filePath = "c://hehe.jar";  
  20.         File file = new File(filePath);  
  21.         URL url = file.toURI().toURL();  
  22.         URL[] urls = new URL[] { url };  
  23.         ClassLoader myClassLoader = new URLClassLoader(urls);  
  24.         return myClassLoader;  
  25.   
  26.     }  

在想象中,這次Test的current classloader應該變成URLClassLoader,並且加載Target成功。但是還是失敗了 

這是因爲前面說過的“雙親委派模型”,URLClassLoader的parent是system classloader,由於工程裏的Test類沒有刪除,所以classpath裏還是能找到Test類,所以Test類的current classloader依然是system classloader,和第一次一樣 

 

接下來把工程裏的Test類也刪除,這次就成功了 

 

5.3 Class.forName() 

前面說的是單個參數的forName()方法,默認使用current ClassLoader 

除此之外,Class類還定義了3個參數的forName()方法,方法簽名如下: 
[java] view plain copy
  1. public static Class<?> forName(String name, boolean initialize,  
  2.                    ClassLoader loader)  
  3.         throws ClassNotFoundException  

這個方法的最後一個參數,可以傳遞一個ClassLoader,會用這個ClassLoader進行加載。這個方法很重要 

比如像JNDI,主體類是在JDK包裏,由bootstrap加載。而SPI的實現類,則是由廠商提供,一般在classpath裏。那麼在JNDI的主體類裏,要加載SPI的實現類,直接用Class.forName()方法肯定是不行的,這時候就要用到3個參數的Class.forName()方法了 

6 ContextClassLoader 

6.1 獲取ClassLoader的API 

前面說過,已經有2種方式可以獲取到ClassLoader的引用 

一種是ClassLoader.getSystemClassLoader(),獲取的是system classloader 

另一種是getClass().getClassLoader(),獲取的是current classloader 

這2種API都只能獲取classloader,沒有辦法用來傳遞 

6.2 傳遞ClassLoader 

每一個thread,都有一個contextClassLoader,並且有getter和setter方法,用來在線程之間傳遞ClassLoader 

有2條默認的規則: 

首先,contextClassLoader默認是繼承的,在父線程中創建子線程,那麼子線程會繼承父線程的contextClassLoader 

其次,主線程,也就是執行main()方法的那個線程,默認的contextClassLoader是system classloader 

6.3 例子 

對上面例子中的Test和Main稍微改一下(Test和Target依然打到jar包裏,然後從工程中刪除,避免被system classloader加載到) 
[java] view plain copy
  1. public class Test {  
  2.   
  3.     public void tryForName() {  
  4.   
  5.         System.out.println("current classloader: "  
  6.                 + getClass().getClassLoader());  
  7.         System.out.println("thread context classloader: "  
  8.                 + Thread.currentThread().getContextClassLoader());  
  9.   
  10.         try {  
  11.             Class.forName("net.kyfxbl.test.cl.Target");  
  12.             System.out.println("load class success");  
  13.         } catch (Exception exc) {  
  14.             exc.printStackTrace();  
  15.         }  
  16.   
  17.     }  
  18. }  

[java] view plain copy
  1. public static void main(String[] args) throws Exception {  
  2.   
  3.         ClassLoader cl = createClassLoader();  
  4.   
  5.         Class<?> startupClass = cl.loadClass("net.kyfxbl.test.cl.Test");  
  6.         final Object startupInstance = startupClass.newInstance();  
  7.   
  8.         new Thread(new Runnable() {  
  9.   
  10.             @Override  
  11.             public void run() {  
  12.                 String methodName = "tryForName";  
  13.                 Class<?>[] paramTypes = new Class[0];  
  14.                 Object[] paramValues = new Object[0];  
  15.                 try {  
  16.                     Method method = startupInstance.getClass().getMethod(  
  17.                             methodName, paramTypes);  
  18.                     method.invoke(startupInstance, paramValues);  
  19.                 } catch (Exception exc) {  
  20.                     exc.printStackTrace();  
  21.                 }  
  22.             }  
  23.         }).start();  
  24.   
  25.     }  

這次的tryForName()方法在一個子線程中被調用,並依次打印出current classloader和contextClassLoader,如圖: 

 

可以看到,子線程繼承了父線程的contextClassLoader 

同時可以注意到,contextClassLoader對Class.forName()方法沒有影響,contextClassLoader只是起到在線程之間傳遞ClassLoader的作用 

6.4 題外話 

從這個例子還可以看出,一個方法在運行時的表現,在編譯期是無法確定的 

在運行時的表現,有時候取決於方法所在的類是被哪個ClassLoader加載;有時候取決於是運行在單線程環境下,還是多線程環境下 

這在編譯期是不可知的,所以在編程的時候,要考慮運行時的情況。比如所謂“線程安全”的類,並不是說它“一定”會運行在多線程環境下,而是說它“可以”運行在多線程環境下 

7 總結 

本文大致總結了ClassLoader的背景知識。掌握了背景,再閱讀tomcat的源碼,基本就不會遇到ClassLoader方面的困難 

本文介紹了ClassLoader的標準體系、雙親委派模型、自定義ClassLoader的方法、以及current classloader和contextClassLoader的概念 

其中最重要的是current classloader和contextClassLoader 

用於獲取ClassLoader的API主要有3種: 

ClassLoader.getSystemClassLoader(); 
Class.getClassLoader(); 
Thread.getContextClassLoader(); 

第一個是靜態方法,返回的永遠是system classloader,也就是說,沒什麼用 
後面2個都是實例方法,一個是返回實例所屬的類的ClassLoader;另一個返回當前線程的contextClassLoader,具體的結果都要在運行時才能確定 

其中,contextClassLoader可以起到傳遞ClassLoader的作用,所以特別重要
發佈了6 篇原創文章 · 獲贊 20 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章