一、前言
畢業至今,已經三年光景,平時基本接觸不到關於類加載器的技術(工作上),相信很多同行在開始工作后很長一段時間,對於類的加載機制都沒有深入的了解過,之前偶然的機會接觸了相關的知識,感覺挺有意思,所以這邊摘抄和收集一下別人的帖子,整理記錄一下,希望對處於java進階的同行有所幫助~
二、類的加載過程
2.1 JVM將類加載過程分為三個步驟:裝載(Load),鏈接(Link)和初始化(Initialize)。鏈接又分為三個步驟,如下圖所示:
1) 裝載:查找並加載類的二進制數據
2) 鏈接:
驗證:確保被加載類的正確性;
准備:為類的靜態變量分配內存,並將其初始化為默認值;
解析:把類中的符號引用轉換為直接引用;
3) 初始化:為類的靜態變量賦予正確的初始值
那為什么我要有驗證這一步驟呢?首先如果由編譯器生成的class文件,它肯定是符合JVM字節碼格式的,但是萬一有高手自己寫一個class文件,讓JVM加載並運行,用於惡意用途,就不妙了,因此這個class文件要先過驗證這一關,不符合的話不會讓它繼續執行的,也是為了安全考慮吧。
准備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存后,先進行鏈接的驗證這一步驟,驗證通過后准備階段,給a分配內存,因為變量a是static的,所以此時a等於int類型的默認初始值0,即a=0,然后到解析(后面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
2.2 類的初始化
類什么時候才被初始化:
1)創建類的實例,也就是new一個對象
2)訪問某個類或接口的靜態變量,或者對該靜態變量賦值
3)調用類的靜態方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一個類的子類(會首先初始化子類的父類)
6)JVM啟動時標明的啟動類,即文件名和類名相同的那個類
只有這6中情況才會導致類的類的初始化。類的初始化步驟:
1)如果這個類還沒有被加載和鏈接,那先進行加載和鏈接
2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類(不適用於接口)
3) 假如類中存在初始化語句(如static變量和static塊),那就依次執行這些初始化語句。
2.3 類的加載
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個這個類的java.lang.Class對象,用來封裝類在方法區類的對象。看下面2圖
類的加載的最終產品是位於堆區中的Class對象。Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口
加載類的方式有以下幾種:
1)從本地系統直接加載
2)通過網絡下載.class文件
3)從zip,jar等歸檔文件中加載.class文件
4)從專有數據庫中提取.class文件
5)將Java源文件動態編譯為.class文件(服務器)
2.4 加載器(來自http://blog.csdn.net/cutesource/article/details/5904501)
JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:
1)Bootstrap ClassLoader
負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類
2)Extension ClassLoader
負責加載java平台中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目錄下的jar包
3)App ClassLoader
負責加載classpath中指定的jar包及目錄中class
4)Custom ClassLoader
屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader,加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類在所有ClassLoader只加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
三、 JVM三種預定義類加載器
JVM預定義有三種類加載器,當一個 JVM啟動的時候,Java缺省開始使用如下三種類加載器:
1)引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。它負責將<Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內存中。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作
2)擴展類加載器(extensions class loader):該類加載器在此目錄里面查找並加載 Java 類。擴展類加載器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量-Djava.ext.dirs指定位置中的類庫加載到內存中。開發者可以直接使用標准擴展類加載器
3)系統類加載器(system class loader):系統類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內存中。開發者可以直接使用系統類加載器。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它
四、 類加載器“雙親委派”機制
4.1 “雙親委派”機制介紹
在這里,需要着重說明的是,JVM在加載類時默認采用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父加載器,依次遞歸,如果父加載器可以完成類加載任務,就成功返回;只有父加載器無法完成此加載任務時,才自己去加載。關於虛擬機默認的雙親委派機制,我們可以從系統類加載器和標准擴展類加載器為例作簡單分析。
圖一 標准擴展類加載器繼承層次圖 圖二 系統類加載器繼承層次圖
通過圖一和圖二我們可以看出,類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:
//加載指定名稱(包括包名)的二進制類型,供用戶調用的接口 public Class<?> loadClass(String name) throws ClassNotFoundException{//…} //加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是,這里的resolve參數不一定真正能達到解析的效果~_~),供繼承用 protected synchronized 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.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機默認采用的雙親委派機制到底是什么模樣:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized 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()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)
現在我們可能會有這樣的疑問:擴展類加載器(ExtClassLoader)的父類加載器被強制設置為null了,那么擴展類加載器為什么還能將加載任務委派給啟動類加載器呢?
圖四 標准擴展類加載器和系統類加載器成員大綱視圖
圖五 擴展類加載器和系統類加載器公共父類成員大綱視圖
通過圖四和圖五可以看出,標准擴展類加載器和系統類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass(…)方法。有關java.lang.ClassLoader中默認的加載委派規則前面已經分析過,如果父加載器為null,則會調用本地方法進行啟動類加載嘗試。所以,圖三中,啟動類加載器、標准擴展類加載器和系統類加載器之間的委派關系事實上是仍就成立的。(在后面的用戶自定義類加載器部分,還會做更深入的分析)。
4.2 類加載雙親委派示例
以上已經簡要介紹了虛擬機默認使用的啟動類加載器、標准擴展類加載器和系統類加載器,並以三者為例結合JDK代碼對JVM默認使用的雙親委派類加載機制做了分析。下面我們就來看一個綜合的例子。首先在eclipse中建立一個簡單的java應用工程,然后寫一個簡單的JavaBean如下:
package classloader.test.bean; public class TestBean { public TestBean() {} }
在現有當前工程中另外建立一測試類(ClassLoaderTest.java)內容如下:
測試一: public class ClassLoaderTest { public static void 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中變量視圖查看驗證。
五、java程序動態擴展方式
Java的連接模型允許用戶運行時擴展引用程序,既可以通過當前虛擬機中預定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時並不知道或者尚未存在的類或者接口,並動態連接它們並進行有選擇的解析。
運行時動態擴展java應用程序有如下兩個途徑:
5.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注冊驅動程序,這樣才能被應用程序使用,這就要求驅動程序類必須被初始化,而不單單被加載.
5.2 用戶自定義類加載器
通過前面的分析,我們可以看出,除了和本地實現密切相關的啟動類加載器之外,包括標准擴展類加載器和系統類加載器在內的所有其他類加載器我們都可以當做自定義類加載器來對待,唯一區別是是否被虛擬機默認使用。前面的內容中已經對java.lang.ClassLoader抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般用戶自定義類加載器的工作流程吧(可以結合后面問題解答一起看):
1)首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則轉入步驟2
2)委派類加載請求給父類加載器(更准確的說應該是雙親類加載器,整個虛擬機中各種類加載器最終會呈現樹狀結構),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉入步驟3
3)調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass(…)導入類型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止加載過程(注意:這里的異常種類不止一種)。
(說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass(…)已有委派邏輯情況下)
六、常見問題分析
6.1 由不同的類加載器加載的指定類型還是相同的類型嗎?
在Java中,一個類用其完全匹配類名(fully qualified class name)作為標識,這里指的完全匹配類名包括包名和類名。但在JVM中一個類用其全名和一個加載類ClassLoader的實例作為唯一標識,不同類加載器加載的類將被置於不同的命名空間。我們可以用兩個自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節碼放置到系統路徑或者擴展路徑中,否則會被系統類加載器或擴展類加載器搶先加載),然后用獲取到的兩個Class實例進行java.lang.Object.equals(…)判斷,將會得到不相等的結果。這個大家可以寫兩個自定義的類加載器去加載相同的自定義類型,然后做個判斷;同時,可以測試加載java.*類型,然后再對比測試一下測試結果。
6.2 在代碼中直接調用Class.forName(String name)方法,到底會觸發哪個類加載器進行類加載行為?
Class.forName(String name)默認會使用調用類的類加載器來進行類加載。我們直接來分析一下對應的jdk的代碼:
//java.lang.Class.java public static 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 //虛擬機本地實現,獲取當前類的類加載器,前面介紹的Class的getClassLoader()也使用此方法 native ClassLoader getClassLoader0();
6.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()方法的實現:
private static synchronized void 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指定位置中的類
6.4 在編寫自定義類加載器時,如果將父類加載器強制設置為null,那么會有什么影響?如果自定義的類加載器不能加載指定類,就肯定會加載失敗嗎?
JVM規范中規定如果用戶自定義的類加載器將父類加載器強制設置為null,那么會自動將啟動類加載器設置為當前用戶自定義類加載器的父類加載器(這個問題前面已經分析過了)。同時,我們可以得出如下結論:
即使用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:問題3和問題4的推斷結論是基於用戶自定義的類加載器本身延續了java.lang.ClassLoader.loadClass(…)默認委派邏輯,如果用戶對這一默認委派邏輯進行了改變,以上推斷結論就不一定成立了,詳見問題5。
6.5 編寫自定義類加載器時,一般有哪些注意點?
1) 一般盡量不要覆寫已有的loadClass(…)方法中的委派邏輯
一般在JDK 1.2之前的版本才這樣做,而且事實證明,這樣做極有可能引起系統默認的類加載器不能正常工作。在JVM規范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發者在開發自定義的類加載器時覆寫findClass(…)邏輯。舉一個例子來驗證該問題:
//用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯) public class WrongClassLoader extends ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { returnthis.findClass(name); } protected Class<?> findClass(String name) throws ClassNotFoundException { //假設此處只是到工程以外的特定目錄D:/library下去加載類 具體實現代碼省略 } }
通過前面的分析我們已經知道,用戶自定義類加載器(WrongClassLoader)的默認的類加載器是系統類加載器,但是現在問題4中的結論就不成立了。大家可以簡單測試一下,現在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程類路徑上的類都加載不上了。
//問題5 測試代碼一 public class WrongClassLoaderTest { public static void 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 WrongClassLoader.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邏輯) public class WrongClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { //假設此處只是到工程以外的特定目錄D:/library下去加載類 //具體實現代碼省略 } } //將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運行測試代碼,輸出結果如下: beans.Account WrongClassLoader@1c78e57
這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。這其中的原因分析,我想這里就不必解釋了,大家應該可以分析的出來了。
2) 正確設置父類加載器
通過上面問題4和問題5的分析我們應該已經理解,個人覺得這是自定義用戶類加載器時最重要的一點,但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎,我想現在大家都可以隨便舉出例子了。
3) 保證findClass(String )方法的邏輯正確性
事先盡量准確理解待定義的類加載器要完成的加載任務,確保最大程度上能夠獲取到對應的字節碼內容。
6.6 如何在運行時判斷系統類加載器能加載哪些路徑下的類?
一是可以直接調用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統類加載器(系統類加載器和擴展類加載器本身都派生自URLClassLoader),調用URLClassLoader中的getURLs()方法可以獲取到;
二是可以直接通過獲取系統屬性java.class.path 來查看當前類路徑上的條目信息 , System.getProperty("java.class.path")
6.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
七、轉載說明
第二節,轉自:http://blog.csdn.net/gjanyanlig/article/details/6818655
第四~第六節,轉自:http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html