深入JVM系列(三)之類加載、類加載器、雙親委派機制與常見問題


一.概述

 

定義:虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型。類加載和連接的過程都是在運行期間完成的。

 

二. 類的加載方式

 
1):本地編譯好的class中直接加載
2):網絡加載:java.net.URLClassLoader可以加載url指定的類
3):從jar、zip等等壓縮文件加載類,自動解析jar文件找到class文件去加載util類
4):從java源代碼文件動態編譯成為class文件
 

三.類加載的時機

 

1. 類加載的生命周期:加載(Loading)-->驗證(Verification)-->准備(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸載(Unloading)

 

2.加載:這有虛擬機自行決定。

3.初始化階段:

a)遇到new、getstatic、putstatic、invokestatic這4個字節碼指令時,如果類沒有進行過初始化,出發初始化操作。

b)使用java.lang.reflect包的方法對類進行反射調用時。

c)當初始化一個類的時候,如果發現其父類還沒有執行初始化則進行初始化。

d)虛擬機啟動時用戶需要指定一個需要執行的主類,虛擬機首先初始化這個主類。

注意:接口與類的初始化規則在第三點不同,接口不要氣所有的父接口都進行初始化。

 

四.類加載的過程

 

4.1.加載

 

a)加載階段的工作

i.通過一個類的全限定名來獲取定義此類的二進制字節流。

ii.將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

iii.在java堆中生成一個代表這個類的java.lang.Class對象,做為方法區這些數據的訪問入口。

b)加載階段完成之后二進制字節流就按照虛擬機所需的格式存儲在方區去中。

 

4.2.驗證

 

這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求。

a)文件格式驗證:驗證字節流是否符合Class文件格式的規范,並且能被當前版本的虛擬機處理。

b)元數據驗證:對字節碼描述的信息進行語義分析,以確保其描述的信息符合java語言規范的要求。

c)字節碼驗證:這個階段的主要工作是進行數據流和控制流的分析。任務是確保被驗證類的方法在運行時不會做出危害虛擬機安全的行為。

d)符號引用驗證:這一階段發生在虛擬機將符號引用轉換為直接引用的時候(解析階段),主要是對類自身以外的信息進行匹配性的校驗。目的是確保解析動作能夠正常執行。

 

4.3.准備

 

准備階段是正式為變量分配內存並設置初始值,這些內存都將在方法區中進行分配,這里的變量僅包括類標量不包括實例變量。

 

4.4.解析

 

解析是虛擬機將常量池的符號引用替換為直接引用的過程。

a)符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任意形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標並不一定已經加載到內存中。

b)直接引用:直接引用可以是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。直接飲用是與內存布局相關的。

c)類或接口的解析

d)字段的解析

e)類方法解析

f)接口方法解析

 

4.5.初始化

 

是根據程序員制定的主觀計划區初始化變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

 

五.JVM三種預定義類型類加載器

 

當一個 JVM 啟動的時候,Java 缺省開始使用如下三種類型類裝入器:

 

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

 

標准擴展(Extension)類加載器:擴展類加載器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 實現的。它負責將

< Java_Runtime_Home >/lib/ext 或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者可以直接使用標准擴展類加載器。

 

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

 

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

 

a. Bootstrap ClassLoader/啟動類加載器

主要負責jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作.

 

b. Extension ClassLoader/擴展類加載器

主要負責jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工作

 

c. System ClassLoader/系統類加載器

主要負責java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工作.

 

d. User Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)

在程序運行期間, 通過java.lang.ClassLoader的子類動態加載class文件, 體現java動態實時類裝入特性.

 

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

 

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


圖一 標准擴展類加載器繼承層次圖

 


圖二 系統類加載器繼承層次圖

 

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

 

 

Java代碼   收藏代碼
  1. //加載指定名稱(包括包名)的二進制類型,供用戶調用的接口  
  2. public Class<?> loadClass(String name) throws ClassNotFoundException{//…}  
  3. //加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是,這里的resolve參數不一定真正能達到解析的效果~_~),供繼承用  
  4. protectedsynchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…}  
  5. //findClass方法一般被loadClass方法調用去加載指定名稱類,供繼承用  
  6. protected Class<?> findClass(String name) throws ClassNotFoundException {//…}  
  7. //定義類型,一般在findClass方法中讀取到對應字節碼后調用,可以看出不可繼承(說明:JVM已經實現了對應的具體功能,解析對應的字節碼,產生對應的內部數據結構放置到方法區,所以無需覆寫,直接調用就可以了)  
  8. protected final Class<?> defineClass(String name, byte[] b, int off, int len)  
  9. throws ClassFormatError{//…}  


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

 

Html代碼   收藏代碼
  1. public Class<?> loadClass(String name)throws ClassNotFoundException {  
  2.        return loadClass(name,false);  
  3. }  
  4. protectedsynchronized Class<?> loadClass(String name,boolean resolve)  
  5.            throws ClassNotFoundException {  
  6.        //首先判斷該類型是否已經被加載  
  7.         Class c = findLoadedClass(name);  
  8.        if (c ==null) {  
  9.            //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載  
  10.            try {  
  11.                if (parent !=null) {  
  12. //如果存在父類加載器,就委派給父類加載器加載  
  13.                     c = parent.loadClass(name,false);  
  14.                 }else {  
  15. //如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)  
  16.                     c = findBootstrapClass0(name);  
  17.                 }  
  18.             }catch (ClassNotFoundException e) {  
  19.        //如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能  
  20.                 c = findClass(name);  
  21.             }  
  22.         }  
  23.        if (resolve) {  
  24.             resolveClass(c);  
  25.         }  
  26.        return c;  
  27.     }  


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



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

 

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

示例代碼:

 

Java代碼   收藏代碼
  1. public static void main(String[] args) {  
  2.    try {  
  3.      System.out.println(ClassLoader.getSystemClassLoader());  
  4.      System.out.println(ClassLoader.getSystemClassLoader().getParent();  
  5.      System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
  6.    } catch (Exception e) {  
  7.        e.printStackTrace();  
  8.    }  
  9. }  




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

 

Java代碼   收藏代碼
  1. 代碼輸出如下:  
  2. sun.misc.Launcher$AppClassLoader@197d257  
  3. sun.misc.Launcher$ExtClassLoader@7259da  
  4. null  


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

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

 

Java代碼   收藏代碼
  1. protected ClassLoader() {  
  2.         SecurityManager security = System.getSecurityManager();  
  3.        if (security !=null) {  
  4.             security.checkCreateClassLoader();  
  5.         }  
  6.        //默認將父類加載器設置為系統類加載器,getSystemClassLoader()獲取系統類加載器  
  7.        this.parent = getSystemClassLoader();  
  8.         initialized =true;  
  9.     }  
  10.    protected ClassLoader(ClassLoader parent) {  
  11.         SecurityManager security = System.getSecurityManager();  
  12.        if (security !=null) {  
  13.             security.checkCreateClassLoader();  
  14.         }  
  15.        //強制設置父類加載器  
  16.        this.parent = parent;  
  17.         initialized =true;  
  18.     }  

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

 

Java代碼   收藏代碼
  1. // The parent class loader for delegation  
  2. e ClassLoaderparent;  

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

 

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

 

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

 

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

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



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

 

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

 

七. 類加載雙親委派示例

 

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

 

Java代碼   收藏代碼
  1. package classloader.test.bean;  
  2.    publicclass TestBean {  
  3.        public TestBean() {}  
  4. }  


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

測試一:

 

Java代碼   收藏代碼
  1. publicclass ClassLoaderTest {  
  2.    publicstaticvoid main(String[] args) {  
  3.        try {  
  4.            //查看當前系統類路徑中包含的路徑條目  
  5.             System.out.println(System.getProperty("java.class.path"));  
  6. //調用加載當前類的類加載器(這里即為系統類加載器)加載TestBean  
  7. Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  
  8. //查看被加載的TestBean類型是被那個類加載器加載的  
  9.             System.out.println(typeLoaded.getClassLoader());  
  10.         }catch (Exception e) {  
  11.             e.printStackTrace();  
  12.         }  
  13.     }  
  14. }  


對應的輸出如下:

 

Java代碼   收藏代碼
  1. D:"DEMO"dev"Study"ClassLoaderTest"bin  
  2. sun.misc.Launcher$AppClassLoader@197d257  


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

 

測試二:

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

 

Java代碼   收藏代碼
  1. D:"DEMO"dev"Study"ClassLoaderTest"bin  
  2. sun.misc.Launcher$ExtClassLoader@7259da  


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

 

測試三:

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

 

Java代碼   收藏代碼
  1. D:"DEMO"dev"Study"ClassLoaderTest"bin  
  2. 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中變量視圖查看驗證。

 

八. 程序動態擴展方式

 

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

 

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

 

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

 

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

 

Java代碼   收藏代碼
  1. public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  


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

 

8.2.用戶自定義類加載器

 

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

 

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

 

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

 

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

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

 

九. 常見問題分析

 

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

 

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

 

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

 

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

 

Java代碼   收藏代碼
  1. //java.lang.Class.java  
  2.        publicstatic Class<?>forName(String className)throws ClassNotFoundException {  
  3. return forName0(className,true, ClassLoader.getCallerClassLoader());  
  4. }  
  5. //java.lang.ClassLoader.java  
  6. // Returns the invoker's class loader, or null if none.  
  7. static ClassLoader getCallerClassLoader() {  
  8.               // 獲取調用類(caller)的類型  
  9.         Class caller = Reflection.getCallerClass(3);  
  10.               // This can be null if the VM is requesting it  
  11.        if (caller ==null) {  
  12.            returnnull;  
  13.         }  
  14.        //調用java.lang.Class中本地方法獲取加載該調用類(caller)的ClassLoader  
  15.        return caller.getClassLoader0();  
  16. }  
  17. //java.lang.Class.java  
  18. //虛擬機本地實現,獲取當前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法  
  19. native ClassLoader getClassLoader0();  


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

 

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

 

Java代碼   收藏代碼
  1. //摘自java.lang.ClassLoader.java  
  2. protected ClassLoader() {  
  3.            SecurityManager security = System.getSecurityManager();  
  4.           if (security !=null) {  
  5.                security.checkCreateClassLoader();  
  6.            }  
  7.           this.parent = getSystemClassLoader();  
  8.            initialized =true;  
  9. }  


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

 

Java代碼   收藏代碼
  1. privatestaticsynchronizedvoid initSystemClassLoader() {  
  2.            //...  
  3.            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
  4.            scl = l.getClassLoader();  
  5.            //...  
  6. }  


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

 

Java代碼   收藏代碼
  1. System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  


本機對應輸出如下:

 

Java代碼   收藏代碼
  1. sun.misc.Launcher$AppClassLoader@197d257  


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

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

1.<Java_Runtime_Home>/lib下的類

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

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

 

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

 

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

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

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

 

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

 

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

 

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

 

Java代碼   收藏代碼
  1. //用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯)  
  2. publicclassWrongClassLoaderextends ClassLoader {  
  3.        public Class<?> loadClass(String name)throws ClassNotFoundException {  
  4.            returnthis.findClass(name);  
  5.         }  
  6.        protected Class<?> findClass(String name)throws ClassNotFoundException {  
  7.            //假設此處只是到工程以外的特定目錄D:/library下去加載類  
  8.            具體實現代碼省略  
  9.         }  
  10. }  


通過前面的分析我們已經知道,用戶自定義類加載器(WrongClassLoader)的默

認的類加載器是系統類加載器,但是現在問題4種的結論就不成立了。大家可以簡

單測試一下,現在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工

程類路徑上的類都加載不上了。

 

Java代碼   收藏代碼
  1. //問題5測試代碼一  
  2. publicclass WrongClassLoaderTest {  
  3.        publicstaticvoid main(String[] args) {  
  4.           try {  
  5.                WrongClassLoader loader =new WrongClassLoader();  
  6.                Class classLoaded = loader.loadClass("beans.Account");  
  7.                System.out.println(classLoaded.getName());  
  8.                System.out.println(classLoaded.getClassLoader());  
  9.            }catch (Exception e) {  
  10.                e.printStackTrace();  
  11.            }  
  12.         }  
  13. }  

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

輸出結果:

 

Java代碼   收藏代碼
  1. java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統找不到指定的路徑。)  
  2.     at java.io.FileInputStream.open(Native Method)  
  3.     at java.io.FileInputStream.<init>(FileInputStream.java:106)  
  4.     at WrongClassLoader.findClass(WrongClassLoader.java:40)  
  5.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  6.     at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
  7.     at java.lang.ClassLoader.defineClass1(Native Method)  
  8.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
  9.     at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
  10.     at WrongClassLoader.findClass(WrongClassLoader.java:43)  
  11.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  12.     at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
  13. Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
  14.     at java.lang.ClassLoader.defineClass1(Native Method)  
  15.     at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
  16.     at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
  17.     at WrongClassLoader.findClass(WrongClassLoader.java:43)  
  18.     at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
  19.     at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  


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

 

Java代碼   收藏代碼
  1. //問題5測試二  
  2. //用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯)  
  3. publicclassWrongClassLoaderextends ClassLoader {  
  4.        protected Class<?> findClass(String name)throws ClassNotFoundException {  
  5.            //假設此處只是到工程以外的特定目錄D:/library下去加載類  
  6.            具體實現代碼省略  
  7.         }  
  8. }  


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

 

Java代碼   收藏代碼
  1. beans.Account  
  2. WrongClassLoader@1c78e57  


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

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

 

9.5.2.正確設置父類加載器

 

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

 

9.5.3.保證findClass(String)方法的邏輯正確性

 

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

 

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

 

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

 

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

 

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

 

方法之一:

 

Java代碼   收藏代碼
  1. try {  
  2.                URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();  
  3.               for (int i = 0; i < extURLs.length; i++) {  
  4.                      System.out.println(extURLs[i]);  
  5.               }  
  6.        } catch (Exception e) {//…}  


本機對應輸出如下:

 

Java代碼   收藏代碼
  1. file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/dnsns.jar  
  2. file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/localedata.jar  
  3. file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunjce_provider.jar  
  4. file:/D:/DEMO/jdk1.5.0_09/jre/lib/ext/sunpkcs11.jar  


十、再分析類加載

 

10.1.類加載器的特性

 

1,每個ClassLoader都維護了一份自己的名稱空間,同一個名稱空間里不能出現兩個同名的類。
2,為了實現java安全沙箱模型頂層的類加載器安全機制, java默認采用了”雙親委派的加載鏈”結構.


如下圖:

Class Diagram:

 

類圖中,BootstrapClassLoader是一個單獨的java類,其實在這里,不應該叫他是一個java類。
因為,它已經完全不用java實現了。

 

它是在jvm啟動時,就被構造起來的,負責java平台核心庫。(具體上面已經有介紹)

啟動類加載實現(其實我們不用關心這塊,但是有興趣的,可以研究一下):
bootstrap classLoader類加載原理探索

 

10.2.自定義類加載器加載一個類的步驟

ClassLoader類加載邏輯分析,以下邏輯是除BootstrapClassLoader外的類加載器加載流程:

 

 

Java代碼   收藏代碼
  1. // 檢查類是否已被裝載過    
  2.   Class c = findLoadedClass(name);    
  3.   if (c == null ) {    
  4.        // 指定類未被裝載過    
  5.        try {    
  6.            if (parent != null ) {    
  7.                // 如果父類加載器不為空, 則委派給父類加載    
  8.                c = parent.loadClass(name, false );    
  9.            } else {    
  10.               // 如果父類加載器為空, 則委派給啟動類加載加載    
  11.               c = findBootstrapClass0(name);    
  12.           }    
  13.       } catch (ClassNotFoundException e) {    
  14.           // 啟動類加載器或父類加載器拋出異常后, 當前類加載器將其    
  15.           // 捕獲, 並通過findClass方法, 由自身加載    
  16.           c = findClass(name);    
  17.       }    
  18.  }    


 

10.3.用Class.forName加載類

 
Class.forName使用的是被調用者的類加載器來加載類的.
這種特性,證明了java類加載器中的名稱空間是唯一的,不會相互干擾.

 

即在一般情況下,保證同一個類中所關聯的其他類都是由當前類的類加載器所加載的.

 

Java代碼   收藏代碼
  1. public static Class forName(String className)    
  2.         throws ClassNotFoundException {    
  3.         return forName0(className, true , ClassLoader.getCallerClassLoader());    
  4.    }    
  5.         
  6.    /** Called after security checks have been made. */    
  7.    private static native Class forName0(String name, boolean initialize,    
  8.    ClassLoader loader)    
  9.         throws ClassNotFoundException;    

 

上圖中ClassLoader.getCallerClassLoader就是得到調用當前forName方法的類的類加載器

 

 

10.4.線程上下文類加載器

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

 

 

 

Java代碼   收藏代碼
  1. // Now create the class loader to use to launch the application    
  2.    try {    
  3.        loader = AppClassLoader.getAppClassLoader(extcl);    
  4.    } catch (IOException e) {    
  5.        throw new InternalError(    
  6.    "Could not create application class loader" );    
  7.    }    
  8.         
  9.    // Also set the context class loader for the primordial thread.    
  10.   Thread.currentThread().setContextClassLoader(loader);    

 

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

使用線程上下文類加載器,可以在執行線程中,拋棄雙親委派加載鏈模式,使用線程上下文里的類加載器加載類.


典型的例子有,通過線程上下文來加載第三方庫jndi實現,而不依賴於雙親委派.

大部分java app服務器(jboss, tomcat..)也是采用contextClassLoader來處理web服務。


還有一些采用hotswap特性的框架,也使用了線程上下文類加載器,比如seasar (full stack framework in japenese).

線程上下文從根本解決了一般應用不能違背雙親委派模式的問題.

使java類加載體系顯得更靈活.

 

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

 

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

 

 

10.5.自定義的類加載器實現

 
defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
是java.lang.Classloader提供給開發人員,用來自定義加載class的接口.

 

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

 

例如,
在jdk中, URLClassLoader是配合findClass方法來使用defineClass,可以從網絡或硬盤上加載class.

而使用類加載接口,並加上自己的實現邏輯,還可以定制出更多的高級特性.

 

比如,

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

 

 

Java代碼   收藏代碼
  1. import java.io.File;    
  2.    import java.io.FileInputStream;    
  3.    import java.lang.reflect.Method;    
  4.    import java.net.URL;    
  5.    import java.net.URLClassLoader;    
  6.         
  7.    /**  
  8.    * 可以重新載入同名類的類加載器實現  
  9.    *  
  10.      
  11.   * 放棄了雙親委派的加載鏈模式.  
  12.   * 需要外部維護重載后的類的成員變量狀態.  
  13.   *  
  14.   * @author ken.wu  
  15.   * @mail ken.wug@gmail.com  
  16.   * 2007-9-28 下午01:37:43  
  17.   */    
  18.   public class HotSwapClassLoader extends URLClassLoader {    
  19.        
  20.       public HotSwapClassLoader(URL[] urls) {    
  21.           super (urls);    
  22.       }    
  23.        
  24.       public HotSwapClassLoader(URL[] urls, ClassLoader parent) {    
  25.           super (urls, parent);    
  26.       }    
  27.        
  28.       public Class load(String name)    
  29.             throws ClassNotFoundException {    
  30.           return load(name, false );    
  31.       }    
  32.        
  33.       public Class load(String name, boolean resolve)    
  34.             throws ClassNotFoundException {    
  35.           if ( null != super .findLoadedClass(name))    
  36.               return reload(name, resolve);    
  37.        
  38.           Class clazz = super .findClass(name);    
  39.        
  40.           if (resolve)    
  41.               super .resolveClass(clazz);    
  42.        
  43.           return clazz;    
  44.       }    
  45.        
  46.       public Class reload(String name, boolean resolve)    
  47.             throws ClassNotFoundException {    
  48.           return new HotSwapClassLoader( super .getURLs(), super .getParent()).load(    
  49.               name, resolve);    
  50.       }    
  51.   }    
  52.        
  53.   public class A {    
  54.       private B b;    
  55.        
  56.       public void setB(B b) {    
  57.            this .b = b;    
  58.       }    
  59.        
  60.       public B getB() {    
  61.            return b;    
  62.       }    
  63.   }    
  64.        
  65.   public class B {}    

 

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

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

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

 

還是老對象的classLoader加載的.

 

Java代碼   收藏代碼
  1. public class TestHotSwap {    
  2.    public static void main(String args[]) {    
  3.        A a = new A();    
  4.        B b = new B();    
  5.        a.setB(b);    
  6.         
  7.        System.out.printf("A classLoader is %s n" , a.getClass().getClassLoader());    
  8.        System.out.printf("B classLoader is %s n" , b.getClass().getClassLoader());    
  9.        System.out.printf("A.b classLoader is %s n" ,   a.getB().getClass().getClassLoader());    
  10.        
  11.       HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\e:\test\")} , a.getClass().getClassLoader());    
  12.       Class clazz = c1.load(" test.hotswap.A ");    
  13.       Object aInstance = clazz.newInstance();    
  14.        
  15.       Method method1 = clazz.getMethod(" setB ", B.class);    
  16.       method1.invoke(aInstance, b);    
  17.        
  18.       Method method2 = clazz.getMethod(" getB ", null);    
  19.       Object bInstance = method2.invoke(aInstance, null);    
  20.        
  21.       System.out.printf(" reloaded A.b classLoader is %s n", bInstance.getClass().getClassLoader());    
  22.   }    
  23.   }    

 

輸出

 

Java代碼   收藏代碼
  1. A classLoader is sun.misc.Launcher$AppClassLoader@19821f  
  2. B classLoader is sun.misc.Launcher$AppClassLoader@19821f  
  3. A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f  
  4. reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@19821f  


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM