Calvin's Tech Space

          成于堅忍,毀于浮躁

             :: 首頁 :: 聯系 :: 聚合  :: 管理

          摘要:

          每個java開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背后就涉及到了java技術體系中的類加載。Java的類加載機制是java技術體系中比較核心的部分,雖然和大部分開發人員直接打交道不多,但是對其背后的機理有一定理解有助于排查程序中出現的類加載失敗等技術問題,對理解java虛擬機的連接模型和java語言的動態性都有很大幫助。

          由于關于java類加載的內容較多,所以打算分三篇文章簡述一下:

          第一篇:java類加載原理解析

          第二篇:插件環境下類加載原理解析

          (參見 深入剖析 Eclipse 類裝入器http://www.ibm.com/developerworks/cn/opensource/os-lo-ecl-classloader/

          第三篇:線程上下文類加載器

          http://www.aygfsteel.com/orangelizq/archive/2009/03/25/261958.html

          2    Java虛擬機類加載器結構簡述

          2.1    JVM三種預定義類型類加載器

          我們首先看一下JVM預定義的三種類型類加載器,當一個 JVM 啟動的時候,Java 缺省開始使用如下三種類型類裝入器:

          啟動(Bootstrap)類加載器引導類裝入器是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib 下面的類庫加載到內存中。由于引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。

          標準擴展(Extension)類加載器擴展類加載器是由 Sun 的 ExtClassLoadersun.misc.Launcher$ExtClassLoader) 實現的。它負責將< Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器。

          系統(System)類加載器系統類加載器是由 Sun 的 AppClassLoadersun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者可以直接使用系統類加載器。

          除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器,這個將在后面單獨介紹。

          2.2    類加載雙親委派機制介紹和分析

          在這里,需要著重說明的是,JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。關于虛擬機默認的雙親委派機制,我們可以從系統類加載器和標準擴展類加載器為例作簡單分析。        

          類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:

          //加載指定名稱(包括包名)的二進制類型,供用戶調用的接口

          public Class<?> loadClass(String name) throws ClassNotFoundException{//…}

          //加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是,這里的resolve參數不一定真正能達到解析的效果~_~),供繼承用

          protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}

          //findClass方法一般被loadClass方法調用去加載指定名稱類,供繼承用

          protected Class<?> findClass(String name) throws ClassNotFoundException {//…}

          //定義類型,一般在findClass方法中讀取到對應字節碼后調用,可以看出不可繼承(說明:JVM已經實現了對應的具體功能,解析對應的字節碼,產生對應的內部數據結構放置到方法區,所以無需覆寫,直接調用就可以了

          protected final Class<?> defineClass(String name, byte[] b, int off, int len)

          throws ClassFormatError{//…}

           通過進一步分析標準擴展類加載器(sun.misc.Launcher$ExtClassLoader)和系統類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(java.net.URLClassLoaderjava.security.SecureClassLoader)的代碼可以看出,都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClassString name)方法的代碼就可以分析出虛擬機默認采用的雙親委派機制到底是什么模樣:

          public Class<?> loadClass(String name)throws ClassNotFoundException {

                  return loadClass(name, false);

          }

          protectedsynchronized Class<?> loadClass(String name, boolean resolve)

                      throws ClassNotFoundException {

                  // 首先判斷該類型是否已經被加載

                  Class c = findLoadedClass(name);

                  if (c == null) {

                      //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載

                      try {

                          if (parent != null) {

          //如果存在父類加載器,就委派給父類加載器加載

                              c = parent.loadClass(name, false);

                          } else {

          //如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)

                              c = findBootstrapClass0(name);

                          }

                      } catch (ClassNotFoundException e) {

                  // 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能

                          c = findClass(name);

                      }

                  }

                  if (resolve) {

                      resolveClass(c);

                  }

                  return c;

              }

          通過上面的代碼分析,我們可以對JVM采用的雙親委派類加載機制有了更感性的認識,下面我們就接著分析一下啟動類加載器、標準擴展類加載器和系統類加載器三者之間的關系。可能大家已經從各種資料上面看到了如下類似的一幅圖片:

                            

                                 圖三 類加載器默認委派關系圖

          上面圖片給人的直觀印象是系統類加載器的父類加載器是標準擴展類加載器,標準擴展類加載器的父類加載器是啟動類加載器,下面我們就用代碼具體測試一下:

          示例代碼

          Public static  void main(String[] args) {

          try {

              System.out.println(ClassLoader.getSystemClassLoader());

          System.out.println(ClassLoader.getSystemClassLoader().getParent();

              System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

          } catch (Exception e) {

              e.printStackTrace();

          }

          }

          說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類加載器。

          代碼輸出如下:

          sun.misc.Launcher$AppClassLoader@197d257

          sun.misc.Launcher$ExtClassLoader@7259da

          null

          通過以上的代碼輸出,我們可以判定系統類加載器的父加載器是標準擴展類加載器,但是我們試圖獲取標準擴展類加載器的父類加載器時確得到了null就是說標準擴展類加載器本身強制設定父類加載器為null。我們還是借助于代碼分析一下:

          我們首先看一下java.lang.ClassLoader抽象類中默認實現的兩個構造函數:

              protected ClassLoader() {

                  SecurityManager security = System.getSecurityManager();

                  if (security != null) {

                      security.checkCreateClassLoader();

                  }

                  //默認將父類加載器設置為系統類加載器,getSystemClassLoader()獲取系統類加載器

                  this.parent = getSystemClassLoader();

                  initialized = true;

              }

              protected ClassLoader(ClassLoader parent) {

                  SecurityManager security = System.getSecurityManager();

                  if (security != null) {

                      security.checkCreateClassLoader();

                  }

                  //強制設置父類加載器

                  this.parent = parent;

                  initialized = true;

              }

              我們再看一下ClassLoader抽象類中parent成員的聲明:

          // The parent class loader for delegation

          private ClassLoader parent;

          聲明為私有變量的同時并沒有對外提供可供派生類訪問的public或者protected設置器接口(對應的setter方法),結合前面的測試代碼的輸出,我們可以推斷出:

          1.系統類加載器(AppClassLoader,使用ClassLoader(ClassLoader parent)構造函數將父類加載器設置為標準擴展類加載器(ExtClassLoader)。(因為如果不強制設置,默認會通過調用getSystemClassLoader()方法獲取并設置成系統類加載器,這顯然和測試輸出結果不符。)

          2.擴展類加載器(ExtClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類加載器設置為null。(因為如果不強制設置,默認會通過調用getSystemClassLoader()方法獲取并設置成系統類加載器,這顯然和測試輸出結果不符。)

          3.現在我們可能會有這樣的疑問:擴展類加載器(ExtClassLoader)的父類加載器被強制設置為null了,那么擴展類加載器為什么還能將加載任務委派給啟動類加載器呢?

          圖四 標準擴展類加載器和系統類加載器成員大綱視圖

                      

                     圖五 擴展類加載器和系統類加載器公共父類成員大綱視圖

          通過圖四和圖五可以看出,標準擴展類加載器和系統類加載器及其父類(java.net.URLClassLoaderjava.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass)方法。有關java.lang.ClassLoader中默認的加載委派規則前面已經分析過,如果父加載器為null,則會調用本地方法進行啟動類加載嘗試。所以,圖三中,啟動類加載器、標準擴展類加載器和系統類加載器之間的委派關系事實上是仍就成立的。(在后面的用戶自定義類加載器部分,還會做更深入的分析)。

          2.3    類加載雙親委派示例

          以上已經簡要介紹了虛擬機默認使用的啟動類加載器、標準擴展類加載器和系統類加載器,并以三者為例結合JDK代碼對JVM默認使用的雙親委派類加載機制做了分析。下面我們就來看一個綜合的例子。首先在eclipse中建立一個簡單的java應用工程,然后寫一個簡單的JavaBean如下:

          package classloader.test.bean;

              publicclass TestBean {

                  public TestBean() {}

          }

          在現有當前工程中另外建立一測試類(ClassLoaderTest.java)內容如下:

          測試一:

          publicclass ClassLoaderTest {

              publicstaticvoid main(String[] args) {

                  try {

                      //查看當前系統類路徑中包含的路徑條目

                      System.out.println(System.getProperty("java.class.path"));

          //調用加載當前類的類加載器(這里即為系統類加載器)加載TestBean

          Class typeLoaded = Class.forName("classloader.test.bean.TestBean");

          //查看被加載的TestBean類型是被那個類加載器加載的

                      System.out.println(typeLoaded.getClassLoader());

                  } catch (Exception e) {

                      e.printStackTrace();

                  }

              }

          }

          對應的輸出如下:

          D:"DEMO"dev"Study"ClassLoaderTest"bin

          sun.misc.Launcher$AppClassLoader@197d257

          (說明:當前類路徑默認的含有的一個條目就是工程的輸出目錄)

          測試二:

          將當前工程輸出目錄下的…/classloader/test/bean/TestBean.class打包進test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果如下:

          D:"DEMO"dev"Study"ClassLoaderTest"bin

          sun.misc.Launcher$ExtClassLoader@7259da

          對比測試一和測試二,我們明顯可以驗證前面說的雙親委派機制,系統類加載器在接到加載classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類加載器(標準擴展類加載器),標準擴展類加載器搶先完成了加載請求。

              測試三:

          test.jar拷貝一份到< Java_Runtime_Home >/lib下,運行測試代碼,輸出如下:

          D:"DEMO"dev"Study"ClassLoaderTest"bin

          sun.misc.Launcher$ExtClassLoader@7259da

             測試三和測試二輸出結果一致。那就是說,放置到< Java_Runtime_Home >/lib目錄下的TestBean對應的class字節碼并沒有被加載,這其實和前面講的雙親委派機制并不矛盾。虛擬機出于安全等因素考慮,不會加載< Java_Runtime_Home >/lib存在的陌生類,開發者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。做個進一步驗證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然后再運行測試代碼,則將會有ClassNotFoundException異常拋出。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點運行測試三進行調試,會發現findBootstrapClass0()會拋出異常,然后在下面的findClass方法中被加載,當前運行的類加載器正是擴展類加載器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變量視圖查看驗證。

          3  java程序動態擴展方式

          Java的連接模型允許用戶運行時擴展引用程序,既可以通過當前虛擬機中預定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時并不知道或者尚未存在的類或者接口,并動態連接它們并進行有選擇的解析。

          運行時動態擴展java應用程序有如下兩個途徑:

          3.1    調用java.lang.Class.forName(…)

          這個方法其實在前面已經討論過,在后面的問題2解答中說明了該方法調用會觸發那個類加載器開始加載任務。這里需要說明的是多參數版本的forName(…)方法:

          public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

          這里的initialize參數是很重要的,可以覺得被加載同時是否完成初始化的工作(說明單參數版本的forName方法默認是完成初始化的).有些場景下,需要將initialize設置為true來強制加載同時完成初始化,例如典型的就是利用DriverManager進行JDBC驅動程序類注冊的問題,因為每一個JDBC驅動程序類的靜態初始化方法都用DriverManager注冊驅動程序,這樣才能被應用程序使用,這就要求驅動程序類必須被初始化,而不單單被加載.

          注意,有一點很有意思,當使用“.class”來創建對Class對象的引用時,不會自動地初始化該Class對象。

          3.2    用戶自定義類加載器

          通過前面的分析,我們可以看出,除了和本地實現密切相關的啟動類加載器之外,包括標準擴展類加載器和系統類加載器在內的所有其他類加載器我們都可以當做自定義類加載器來對待,唯一區別是是否被虛擬機默認使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般用戶自定義類加載器的工作流程吧(可以結合后面問題解答一起看):

          1、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則轉入步驟2

          2、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真個虛擬機中各種類加載器最終會呈現樹狀結構),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉入步驟3

          3調用本類加載器的findClass)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass)導入類型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass), loadClass)轉拋異常,終止加載過程(注意:這里的異常種類不止一種)。

             (說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下)

          4       常見問題分析:

          4.1    由不同的類加載器加載的指定類型還是相同的類型嗎?

          Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這里指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識,不同類加載器加載的類將被置于不同的命名空間.我們可以用兩個自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節碼放置到系統路徑或者擴展路徑中,否則會被系統類加載器或擴展類加載器搶先加載),然后用獲取到的兩個Class實例進行java.lang.Object.equals)判斷,將會得到不相等的結果。這個大家可以寫兩個自定義的類加載器去加載相同的自定義類型,然后做個判斷;同時,可以測試加載java.*類型,然后再對比測試一下測試結果。

          4.2    在代碼中直接調用Class.forNameString name)方法,到底會觸發那個類加載器進行類加載行為?

          Class.forName(String name)默認會使用調用類的類加載器來進行類加載。我們直接來分析一下對應的jdk的代碼:

          //java.lang.Class.java

                 publicstatic Class<?> forName(String className)throws ClassNotFoundException {

          return forName0(className, true, ClassLoader.getCallerClassLoader());

          }

          //java.lang.ClassLoader.java

          // Returns the invoker's class loader, or null if none.

          static ClassLoader getCallerClassLoader() {

                        // 獲取調用類(caller)的類型

                  Class caller = Reflection.getCallerClass(3);

                        // This can be null if the VM is requesting it

                  if (caller == null) {

                      Return null;

                  }

                  // 調用java.lang.Class中本地方法獲取加載該調用類(caller)的ClassLoader

                  return caller.getClassLoader0();

          }

          //java.lang.Class.java

          //虛擬機本地實現,獲取當前類的類加載器,前面介紹的ClassgetClassLoader()也使用此方法

          native ClassLoader getClassLoader0();

          4.3    在編寫自定義類加載器時,如果沒有設定父加載器,那么父加載器是?

          前面講過,在不指定父類加載器的情況下,默認采用系統類加載器。可能有人覺得不明白,現在我們來看一下JDK對應的代碼實現。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對應的無參默認構造函數實現如下:

          //摘自java.lang.ClassLoader.java

          protected ClassLoader() {

                     SecurityManager security = System.getSecurityManager();

                     if (security != null) {

                         security.checkCreateClassLoader();

                     }

                     this.parent = getSystemClassLoader();

                     initialized = true;

          }

          我們再來看一下對應的getSystemClassLoader()方法的實現:

          privatestaticsynchronizedvoid initSystemClassLoader() {

                     //...

                     sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

                     scl = l.getClassLoader();

                     //...

          }

          我們可以寫簡單的測試代碼來測試一下:

          System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

          本機對應輸出如下:

          sun.misc.Launcher$AppClassLoader@197d257

          所以,我們現在可以相信當自定義類加載器沒有指定父類加載器的情況下,默認的父類加載器即為系統類加載器。同時,我們可以得出如下結論:

          即用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個地方的類:

          1.    <Java_Runtime_Home>/lib下的類

          2.    < Java_Runtime_Home >/lib/ext下或者由系統變量java.ext.dir指定位置中的類

          3.    當前工程類路徑下或者由系統變量java.class.path指定位置中的類

          4.4 在編寫自定義類加載器時,如果將父類加載器強制設置為null,那么會有什么影響?如果自定義的類加載器不能加載指定類,就肯定會加載失敗嗎?

          JVM規范中規定如果用戶自定義的類加載器將父類加載器強制設置為null,那么會自動將啟動類加載器設置為當前用戶自定義類加載器的父類加載器(這個問題前面已經分析過了)。同時,我們可以得出如下結論:

          即用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。

          說明:問題3和問題4的推斷結論是基于用戶自定義的類加載器本身延續了java.lang.ClassLoader.loadClass)默認委派邏輯,如果用戶對這一默認委派邏輯進行了改變,以上推斷結論就不一定成立了,詳見問題5

          4.5    編寫自定義類加載器時,一般有哪些注意點?

          1. 一般盡量不要覆寫已有的loadClass)方法中的委派邏輯

          一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統默認的類加載器不能正常工作。在JVM規范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:

          //用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯)

          Public class WrongClassLoaderextends ClassLoader {

                  public Class<?> loadClass(String name) throws ClassNotFoundException {

                      Return  this.findClass(name);

                  }

                  protected Class<?> findClass(String name) throws ClassNotFoundException {

                      //假設此處只是到工程以外的特定目錄D:/library下去加載類

                      具體實現代碼省略

                  }

          }

              通過前面的分析我們已經知道,用戶自定義類加載器(WrongClassLoader)的默認的類加載器是系統類加載器,但是現在問題4種的結論就不成立了。大家可以簡單測試一下,現在<Java_Runtime_Home>/lib< Java_Runtime_Home >/lib/ext和工程類路徑上的類都加載不上了。

                 //問題5測試代碼一

          publicclass WrongClassLoaderTest {

                  publicstaticvoid main(String[] args) {

                     try {

                         WrongClassLoader loader = new WrongClassLoader();

                         Class classLoaded = loader.loadClass("beans.Account");

                         System.out.println(classLoaded.getName());

                         System.out.println(classLoaded.getClassLoader());

                     } catch (Exception e) {

                         e.printStackTrace();

                     }

                  }

          }

          (說明:D:"classes"beans"Account.class物理存在的)

          輸出結果:

          java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)

              at java.io.FileInputStream.open(Native Method)

              at java.io.FileInputStream.<init>(FileInputStream.java:106)

              at WrongClassLoader.findClass(WrongClassLoader.java:40)

              at WrongClassLoader.loadClass(WrongClassLoader.java:29)

              at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)

              at java.lang.ClassLoader.defineClass1(Native Method)

              at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

              at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

              at WrongClassLader.findClass(WrongClassLoader.java:43)

              at WrongClassLoader.loadClass(WrongClassLoader.java:29)

          at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27

          Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object

              at java.lang.ClassLoader.defineClass1(Native Method)

              at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

              at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

              at WrongClassLoader.findClass(WrongClassLoader.java:43)

              at WrongClassLoader.loadClass(WrongClassLoader.java:29)

              at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

          這說明,連要加載的類型的超類型java.lang.Object都加載不到了。這里列舉的由于覆寫loadClass)引起的邏輯錯誤明顯是比較簡單的,實際引起的邏輯錯誤可能復雜的多。

          //問題5測試代碼

          //用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯)

          publicclassWrongClassLoaderextends ClassLoader {

                  protected Class<?> findClass(String name) throws ClassNotFoundException {

                      //假設此處只是到工程以外的特定目錄D:/library下去加載類

                      具體實現代碼省略

                  }

          }

          將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出結果如下:

          beans.Account

          WrongClassLoader@1c78e57

          這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。

          這其中的原因分析,我想這里就不必解釋了,大家應該可以分析的出來了。

          2正確設置父類加載器

          通過上面問題4和問題5的分析我們應該已經理解,個人覺得這是自定義用戶類加載器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎,我想現在大家都可以隨便舉出例子了。

          3、保證findClassString )方法的邏輯正確性

          事先盡量準確理解待定義的類加載器要完成的加載任務,確保最大程度上能夠獲取到對應的字節碼內容。

          4.6 如何在運行時判斷系統類加載器能加載哪些路徑下的類?

          一是可以直接調用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類加載器(系統類加載器和擴展類加載器本身都派生自URLClassLoader),調用URLClassLoader中的getURLs()方法可以獲取到;

          URLClassLoader loader = (URLClassLoader)ClassLoader.getSystemClassLoader();

          for(URL url:loader.getURLs()){

          System.out.println(url.getPath());

          }

          二是可以直接通過獲取系統屬性java.class.path 來查看當前類路徑上的條目信息 , System.getProperty("java.class.path")

          4.7 如何在運行時判斷標準擴展類加載器能加載哪些路徑下的類?

          方法之一:

          try {

                         URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();

                        for (int i = 0; i < extURLs.length; i++) {

                               System.out.println(extURLs[i]);

                        }

                 } catch (Exception e) {//…}

                 本機對應輸出如下:

          file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar

          file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar

          file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar

          file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar

          總結:

          寫這篇文章的初衷是通過分析JDK相關代碼來驗證一些加載規則,核心就是借助雙親委派機制來分析一個加載請求處理的主要過程,所列舉的幾個簡單的例子實際意義不大,因為遇到的情況往往比例子情況復雜的多。

          下一篇文章,我會重點分析一下Eclipse的插件類加載器,并分析一下插件環境下的類加載和普通java應用場景中的類加載有什么不同,并會提供一個比較完整的類加載器。

          插件類加載器就是一個由eclipse開發的一個用戶自定義類加載器,所以分析時候用到的一些基本的東西都是在這篇文章中涉及到的。

          這篇文章寫的時候時間比較緊,亂糟糟的,大家見諒。中間參考了JVM規范、jdk文檔和代碼、《Inside Java Virtual Machine》一書等資料。

          另注:

          Java jar文件分可執行jar 和非可執行jar 

          一個可執行的 JAR 必須通過 menifest 文件的頭引用它所需要的所有其他從屬 JAR。如果使用了 -jar 選項,那么環境變量 CLASSPATH 和在命令行中指定的所有類路徑都被 JVM 所忽略,如果依賴的class包含在一個JAR文件中,那么需要把這個JAR文件加到類路徑里面去,單獨放到當前目錄是不行的(即使已經將當前目錄包含在類路徑里面了),或者將這個JAR文件考到C:\Program Files\Java\jdk1.6.0_12\jre\lib\ext目錄下,在這種情況下,這個類將有標準擴展類加載器來加載。

          (參見http://www-128.ibm.com/developerworks/cn/java/j-jar/index.html

          為了使用類而做的準備工作實際包含三個步驟: 

          1.加載這是由類加載器執行的。該步驟將查找字節碼,并從這些字節碼中創建一個Class對象。 

          2.鏈接 。這個階段將驗證類中的字節碼,為靜態域分配存儲空間,并且如果必需的話,將解析這個類創建的對其他類的所有引用。 

          3.初始化。如果該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化塊。

          初始化被延遲到對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行:

          線程上下文類加載器

          上面的類裝入器的向上代理結構看上去很完美了,但是,當系統變得復雜的時候,就還是顯得不夠用了。

          例如,當 Java 引入了 JNDI 以后,JNDI 核心部分是通過引導 類裝入器在 JVM 啟動的時候裝載進入 JVM 的。而 JDNI 核心部分是通過配置信息來在運行時候裝載定義在用戶的類路徑中的特定類來完成特定需要。而這是上面定義的類裝入器的向上代理模式所不能支持的。

          為了解決這個問題,Java 2 中引入了線程上下文(Thread Content)類裝入器的概念,每一個線程有一個 Context 類裝入器。這個 Context 類裝入器是通過方法 Thread.setContextClassLoader() 設置的,如果當前線程在創建后沒有調用這個方法設置 Context 類裝入器,則當前線程從他的父線程繼承 Context 類裝入器。如果整個應用都沒有設置 Context 類裝入器,則系統類裝入器被設置為所有線程的 Context 類裝入器。

          對于我們上面所說 JNDI 的情況,引導 類裝入器裝載進入的 JNDI 核心類會使用 Context 類裝入器來裝載其所需要的 JNDI 實現類,而不是將該裝載任務代理給其父類裝入器來完成。這樣,就解決了上面的問題。可以認為 Context 類裝入器在傳統的 Java 向上代理機制上打開了一個后門。Context 類裝入器在 J2EE 中使用的很廣泛,比如 Java 命名服務(JNDI),Java API for XML Parsing(JAXP)(注:在 Java1.4 中 JAXP 才作為 Java 的核心類的一部分,它才開始使用 Context 類裝入器來加載不同的實現類)等。

          java默認的線程上下文類加載器是系統類加載器(AppClassLoader).

          java 代碼

          // Now create the class loader to use to launch the application 

          try { 

              loader = AppClassLoader.getAppClassLoader(extcl); 

          } catch (IOException e) { 

              throw new InternalError( 

          "Could not create application class loader" ); 

          // Also set the context class loader for the primordial thread. 

          Thread.currentThread().setContextClassLoader(loader); 

          以上代碼摘自sun.misc.Launch的無參構造函數Launch()

          使用線程上下文類加載器可以在執行線程中拋棄雙親委派加載鏈模式使用線程上下文里的類加載器加載類.典型的例子有通過線程上下文來加載第三方庫jndi實現而不依賴于雙親委派.大部分java app服務器(jboss, tomcat..)也是采用contextClassLoader來處理web服務。還有一些采用 hotswap 特性的框架也使用了線程上下文類加載器比如 seasar (full stack framework in japenese).

          線程上下文從根本解決了一般應用不能違背雙親委派模式的問題.使java類加載體系顯得更靈活.

          隨著多核時代的來臨相信多線程開發將會越來越多地進入程序員的實際編碼過程中因此,在編寫基礎設施時, 通過使用線程上下文來加載類應該是一個很好的選擇

          當然好東西都有利弊使用線程上下文加載類也要注意保證多根需要通信的線程間的類加載器應該是同一個防止因為不同的類加載器導致類型轉換異常(ClassCastException).

          自定義的類加載器實現

          defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)

          java.lang.Classloader提供給開發人員用來自定義加載class的接口.

          使用該接口可以動態的加載class文件

          例如,

          jdk, URLClassLoader是配合findClass方法來使用defineClass, 可以從網絡或硬盤上加載class.而使用類加載接口并加上自己的實現邏輯還可以定制出更多的高級特性.

          比如

          一個簡單的hot swap 類加載器實現:

          java 代碼

          import java.io.File; 

          import java.io.FileInputStream; 

          import java.lang.reflect.Method; 

          import java.net.URL; 

          import java.net.URLClassLoader; 

          /** 

          可以重新載入同名類的類加載器實現 

          放棄了雙親委派的加載鏈模式

          需要外部維護重載后的類的成員變量狀態

          * @author ken.wu 

          * @mail ken.wug@gmail.com 

          * 2007-9-28 下午01:37:43 

          */ 

          public class HotSwapClassLoader extends URLClassLoader { 

              public HotSwapClassLoader(URL[] urls) { 

                  super (urls); 

              } 

              public HotSwapClassLoader(URL[] urls, ClassLoader parent) { 

                  super (urls, parent); 

              } 

              public Class load(String name) 

                    throws ClassNotFoundException { 

                  return load(name, false ); 

              } 

              public Class load(String name, boolean resolve) 

                    throws ClassNotFoundException { 

                  if ( null != super .findLoadedClass(name)) 

                      return reload(name, resolve); 

                  Class clazz = super .findClass(name); 

                  if (resolve) 

                      super .resolveClass(clazz); 

                  return clazz; 

              } 

              public Class reload(String name, boolean resolve) 

                    throws ClassNotFoundException { 

                  return new HotSwapClassLoader( super .getURLs(), super .getParent()).load( 

                      name, resolve); 

              } 

          java 代碼

          public class A { 

              private B b; 

              public void setB(B b) { 

                   this .b = b; 

              } 

              public B getB() { 

                   return b; 

              } 

           

          java 代碼

          public class B {} 

          這個類的作用是可以重新載入同名的類,但是,為了實現hotswap, 老的對象狀態需要通過其他方式拷貝到重載過的類生成的全新實例中來。(A類中的b實例)

          而新實例所依賴的B類如果與老對象不是同一個類加載器加載的, 將會拋出類型轉換異常(ClassCastException).

          為了解決這種問題, HotSwapClassLoader自定義了load方法即當前類是由自身classLoader加載的,而內部依賴的類還是老對象的classLoader加載的.

          java 代碼

          public class TestHotSwap { 

          public static void main(String args[]) { 

              A a = new A(); 

              B b = new B(); 

              a.setB(b); 

              System.out.printf("A classLoader is %s \n" , a.getClass().getClassLoader()); 

              System.out.printf("B classLoader is %s \n" , b.getClass().getClassLoader()); 

              System.out.printf("A.b classLoader is %s \n" ,   a.getB().getClass().getClassLoader()); 

              HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\\e:\\test\\")} , a.getClass().getClassLoader()); 

              Class clazz = c1.load(" test.hotswap.A "); 

              Object aInstance = clazz.newInstance(); 

              Method method1 = clazz.getMethod(" setB ", B.class); 

              method1.invoke(aInstance, b); 

              Method method2 = clazz.getMethod(" getB ", null); 

              Object bInstance = method2.invoke(aInstance, null); 

              System.out.printf(" reloaded A.b classLoader is %s \n", bInstance.getClass().getClassLoader()); 

          輸出

          A classLoader is sun.misc.Launcher$AppClassLoader@19821f 

          B classLoader is sun.misc.Launcher$AppClassLoader@19821f 

          A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f 

          reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f

          posted on 2009-09-17 12:46 calvin 閱讀(336) 評論(0)  編輯  收藏 所屬分類: Java
          主站蜘蛛池模板: 桓台县| 楚雄市| 宣化县| 东莞市| 贵港市| 江西省| 建瓯市| 湾仔区| 宿迁市| 东方市| 青河县| 兰州市| 浦江县| 东台市| 巴东县| 阳谷县| 安仁县| 恩平市| 诸城市| 蚌埠市| 中西区| 马关县| 石狮市| 屯留县| 贡嘎县| 化州市| 泽州县| 四川省| 阳曲县| 伊宁市| 南丰县| 方山县| 北碚区| 黔东| 潜山县| 茶陵县| 永德县| 襄垣县| 吉林省| 宣城市| 福鼎市|