類加載的過程
類加載器的主要工作就是把類文件加載到JVM中。如下圖所示,其過程分為三步:
1.加載:定位要加載的類文件,并將其字節(jié)流裝載到JVM中;
2.鏈接:給要加載的類分配最基本的內(nèi)存結(jié)構(gòu)保存其信息,比如屬性,方法以及引用的類。在該階段,該類還處于不可用狀態(tài);
(1)驗(yàn)證:對(duì)加載的字節(jié)流進(jìn)行驗(yàn)證,比如格式上的,安全方面的;
(2)內(nèi)存分配:為該類準(zhǔn)備內(nèi)存空間來表示其屬性,方法以及引用的類;
(3)解析:加載該類所引用的其它類,比如父類,實(shí)現(xiàn)的接口等。
3.初始化:對(duì)類變量進(jìn)行賦值。
類加載器的層級(jí)
下圖虛線以上是JDK提供的幾個(gè)重要的類加載器,詳細(xì)說明如下:
(1)Bootstrap Class Loader: 當(dāng)啟動(dòng)包含主函數(shù)的類時(shí),加載JAVA_HOME/lib目錄下或-Xbootclasspath指定目錄的jar包;
(2)Extention Class Loader:加載JAVA_HOME/lib/ext目錄下的或-Djava.ext.dirs指定目錄下的jar包。
(3)System Class Loader:加載classpath或者-Djava.class.path指定目錄下的類或jar包。
需要了解的是:
1.除了Bootstrap Class Loader外,其它的類加載器都是????java.lang.ClassLoader類的子類;
2.Bootstrap Class Loader不是用Java實(shí)現(xiàn),如果你沒有使用個(gè)性化類加載器,那么java.lang.String.class.getClassLoader()就為null,Extension Class Loader的父加載器也為null;
3.獲得類加載器的幾種方式:
(1)獲得Bootstrap Class Loader:試圖獲得Bootstrap Class Loader,得到的必然是null。可以用如下方式驗(yàn)證下:使用rt.jar包內(nèi)的類對(duì)象的getClassLoader方法,比如java.lang.String.class.getClassLoader()可以得到或者獲得Extention Class Loader,再調(diào)用getParent方法獲得;
(2)獲得Extention Class Loader:使用JAVA_HOME/lib/ext目錄下jar包內(nèi)的類對(duì)象的getClassLoader方法或者先獲得System Class Loader,再通過它的getParent方法獲得;
(3)獲得System Class Loader:調(diào)用包含主函數(shù)的類對(duì)象的getClassLoader方法或者在主函數(shù)內(nèi)調(diào)用Thread.currentThread().getContextClassLoader()或者調(diào)用ClassLoader.getSystemClassLoader();
(4)獲得User-Defined Class Loader:調(diào)用類對(duì)象的getClassLoader方法或者調(diào)用Thread.currentThread().getContextClassLoader();
類加載器的操作原則
1.代理原則
2.可見性原則
3.唯一性原則
4.代理原則
代理原則指的是一個(gè)類加載器在加載一個(gè)類時(shí)會(huì)請(qǐng)求它的父加載器代理加載,父加載器也會(huì)請(qǐng)求它的父加載器代理加載,如下圖所示。
為什么要使用代理模式呢?首先這樣可以減少重復(fù)的加載一個(gè)類。(還有其它原因嗎?)
容易誤解的地方:
一般會(huì)以為類加載器的代理順序是Parent First的,也就是:
1.加載一個(gè)類時(shí),類加載器首先檢查自己是否已經(jīng)加載了該類,如果已加載,則返回;否則請(qǐng)父加載器代理;
2.父加載器重復(fù)1的操作一直到Bootstrap Class Loader;
3.如果Bootstrap Class Loader也沒有加載該類,將嘗試進(jìn)行加載,加載成功則返回;如果失敗,拋出ClassNotFoundException,則由子加載器進(jìn)行加載;
4.子類加載器捕捉異常后嘗試加載,如果成功則返回,如果失敗則拋出ClassNotFoundException,直到發(fā)起加載的子類加載器。
這種理解對(duì)Bootstrap Class Loader,Extention Class Loader,System Class Loader這些加載器是正確的,但一些個(gè)性化的加載器則不然,比如,IBM Web Sphere Portal Server實(shí)現(xiàn)的一些類加載器就是Parent Last的,是子加載器首先嘗試加載,如果加載失敗才會(huì)請(qǐng)父加載器,這樣做的原因是:假如你期望某個(gè)版本log4j被所有應(yīng)用使用,就把它放在WAS_HOME的庫里,WAS啟動(dòng)時(shí)會(huì)加載它。如果某個(gè)應(yīng)用想使用另外一個(gè)版本的log4j,如果使用Parent First,這是無法實(shí)現(xiàn)的,因?yàn)楦讣虞d器里已經(jīng)加載了log4j內(nèi)的類。但如果使用Parent Last,負(fù)責(zé)加載應(yīng)用的類加載器會(huì)優(yōu)先加載另外一個(gè)版本的log4j。
可見性原則
每個(gè)類對(duì)類加載器的可見性是不一樣的,如下圖所示。
擴(kuò)展知識(shí),OSGi就是利用這個(gè)特點(diǎn),每一個(gè)bundle由一個(gè)單獨(dú)的類加載器加載,因此每個(gè)類加載器都可以加載某個(gè)類的一個(gè)版本,因此整個(gè)系統(tǒng)就可以使用一個(gè)類的多個(gè)版本。
唯一性原則
每一個(gè)類在一個(gè)加載器里最多加載一次。
擴(kuò)展知識(shí)1:準(zhǔn)確地講,Singleton模式所指的單例指的是在一組類加載器中某個(gè)類的對(duì)象只有一份。
擴(kuò)展知識(shí)2:一個(gè)類可以被多個(gè)類加載器加載,每個(gè)類對(duì)象在各自的namespace內(nèi),對(duì)類對(duì)象進(jìn)行比較或者對(duì)實(shí)例進(jìn)行類型轉(zhuǎn)換時(shí),會(huì)同時(shí)比較各自的名字空間,比如:
Klass類被ClassLoaderA加載,假設(shè)類對(duì)象為KlassA;同時(shí)被ClassLoaderB加載,假設(shè)類對(duì)象為KlassB,那么KlassA不等于KlassB。同時(shí)ClassA的實(shí)例被cast成KlassB時(shí)會(huì)拋出ClassCastException異常。
為什么要個(gè)性化類加載器
個(gè)性化類加載器給Java語言增加了很多靈活性,主要的用途有:
1.可以從多個(gè)地方加載類,比如網(wǎng)絡(luò)上,數(shù)據(jù)庫中,甚至即時(shí)的編譯源文件獲得類文件;
2.個(gè)性化后類加載器可以在運(yùn)行時(shí)原則性的加載某個(gè)版本的類文件;
3.個(gè)性化后類加載器可以動(dòng)態(tài)卸載一些類;
4.個(gè)性化后類加載器可以對(duì)類進(jìn)行解密解壓縮后再載入類。
類的隱式和顯式加載
隱式加載:當(dāng)一個(gè)類被引用,被繼承或者被實(shí)例化時(shí)會(huì)被隱式加載,如果加載失敗,是拋出NoClassDefFoundError。
顯式加載:使用如下方法,如果加載失敗會(huì)拋出ClassNotFoundException。
cl.loadClass(),cl是類加載器的一個(gè)實(shí)例;
Class.forName(),使用當(dāng)前類的類加載器進(jìn)行加載。
類的靜態(tài)塊的執(zhí)行
假如有如下類:
1
2
3
4
5
6
7
|
package cn.fengd; public class Dummy { static { System.out.println( "Hi" ); } } |
另建一個(gè)測(cè)試類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package cn.fengd; public class ClassLoaderTest { public static void main(String[] args) throws InstantiationException, Exception { try { /* * Different ways of loading. */ Class c = ClassLoaderTest. class .getClassLoader().loadClass( "cn.fengd.Dummy" ); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } |
運(yùn)行后效果如何呢?
-
不會(huì)輸出Hi。由此可見使用loadClass后Class類對(duì)象并沒有初始化;
-
如果在Load語句后加上c.newInstance(); 就會(huì)有Hi輸出,對(duì)該類進(jìn)行實(shí)例化時(shí)才初始化類對(duì)象。
-
如果換一種加載語句Class c = Class.forName("cn.fengd.Dummy", false, ClassLoader.getSystemClassLoader());
-
不會(huì)輸出Hi。因?yàn)閰?shù)false表示不需要初始化該類對(duì)象;
-
如果在Load語句后加上c.newInstance(); 就會(huì)有Hi輸出,對(duì)該類進(jìn)行實(shí)例化時(shí)才初始化類對(duì)象。
如果換成Class.forName("cn.fengd.Dummy");或者new Dummy()呢?
都會(huì)輸出Hi。
常見問題分析:
1.由不同的類加載器加載的指定類型還是相同的類型嗎?
在Java中,一個(gè)類用其完全匹配類名(fully qualified class name)作為標(biāo)識(shí),這里指的完全匹配類名包括包名和類名。但在JVM中一個(gè)類用其全名和一個(gè)加載類ClassLoader的實(shí)例作為唯一標(biāo)識(shí),不同類加載器加載的類將被置于不同的命名空間.我們可以用兩個(gè)自定義類加載器去加載某自定義類型(注意,不要將自定義類型的字節(jié)碼放置到系統(tǒng)路徑或者擴(kuò)展路徑中,否則會(huì)被系統(tǒng)類加載器或擴(kuò)展類加載器搶先加載),然后用獲取到的兩個(gè)Class實(shí)例進(jìn)行java.lang.Object.equals(…)判斷,將會(huì)得到不相等的結(jié)果。這個(gè)大家可以寫兩個(gè)自定義的類加載器去加載相同的自定義類型,然后做個(gè)判斷;同時(shí),可以測(cè)試加載java.*類型,然后再對(duì)比測(cè)試一下測(cè)試結(jié)果。
2.在代碼中直接調(diào)用Class.forName(String name)方法,到底會(huì)觸發(fā)那個(gè)類加載器進(jìn)行類加載行為?
Class.forName(String name)默認(rèn)會(huì)使用調(diào)用類的類加載器來進(jìn)行類加載。我們直接來分析一下對(duì)應(yīng)的jdk的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//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() { // 獲取調(diào)用類(caller)的類型 Class caller = Reflection.getCallerClass( 3 ); // This can be null if the VM is requesting it if (caller == null ) { returnnull; } // 調(diào)用java.lang.Class中本地方法獲取加載該調(diào)用類(caller)的ClassLoader return caller.getClassLoader0(); } //java.lang.Class.java //虛擬機(jī)本地實(shí)現(xiàn),獲取當(dāng)前類的類加載器 native ClassLoader getClassLoader0(); |
3.在編寫自定義類加載器時(shí),如果沒有設(shè)定父加載器,那么父加載器是?
在不指定父類加載器的情況下,默認(rèn)采用系統(tǒng)類加載器。可能有人覺得不明白,現(xiàn)在我們來看一下JDK對(duì)應(yīng)的代碼實(shí)現(xiàn)。眾所周知,我們編寫自定義的類加載器直接或者間接繼承自java.lang.ClassLoader抽象類,對(duì)應(yīng)的無參默認(rèn)構(gòu)造函數(shù)實(shí)現(xiàn)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null ) { security.checkCreateClassLoader(); } this .parent = getSystemClassLoader(); initialized = true ; } |
我們?cè)賮砜匆幌聦?duì)應(yīng)的getSystemClassLoader()方法的實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
|
privatestaticsynchronizedvoid initSystemClassLoader() { //... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); scl = l.getClassLoader(); //... } |
我們可以寫簡(jiǎn)單的測(cè)試代碼來測(cè)試一下:
1
|
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader()); |
本機(jī)對(duì)應(yīng)輸出如下:
1
|
sun.misc.Launcher$AppClassLoader@197d257 |
所以,我們現(xiàn)在可以相信當(dāng)自定義類加載器沒有指定父類加載器的情況下,默認(rèn)的父類加載器即為系統(tǒng)類加載器。同時(shí),我們可以得出如下結(jié)論:
即時(shí)用戶自定義類加載器不指定父類加載器,那么,同樣可以加載如下三個(gè)地方的類:
(1)<Java_Runtime_Home>/lib下的類
(2)< Java_Runtime_Home >/lib/ext下或者由系統(tǒng)變量java.ext.dir指定位置中的類
(3)當(dāng)前工程類路徑下或者由系統(tǒng)變量java.class.path指定位置中的類
4.在編寫自定義類加載器時(shí),如果將父類加載器強(qiáng)制設(shè)置為null,那么會(huì)有什么影響?如果自定義的類加載器不能加載指定類,就肯定會(huì)加載失敗嗎?
JVM規(guī)范中規(guī)定如果用戶自定義的類加載器將父類加載器強(qiáng)制設(shè)置為null,那么會(huì)自動(dòng)將啟動(dòng)類加載器設(shè)置為當(dāng)前用戶自定義類加載器的父類加載器(這個(gè)問題前面已經(jīng)分析過了)。同時(shí),我們可以得出如下結(jié)論:
即時(shí)用戶自定義類加載器不指定父類加載器,那么,同樣可以加載到<Java_Runtime_Home>/lib下的類,但此時(shí)就不能夠加載<Java_Runtime_Home>/lib/ext目錄下的類了。
說明:?jiǎn)栴}3和問題4的推斷結(jié)論是基于用戶自定義的類加載器本身延續(xù)了java.lang.ClassLoader.loadClass(…)默認(rèn)委派邏輯,如果用戶對(duì)這一默認(rèn)委派邏輯進(jìn)行了改變,以上推斷結(jié)論就不一定成立了,詳見問題5。
5.編寫自定義類加載器時(shí),一般有哪些注意點(diǎn)?
(1)一般盡量不要覆寫已有的loadClass(…)方法中的委派邏輯
一般在JDK 1.2之前的版本才這樣做,而且事實(shí)證明,這樣做極有可能引起系統(tǒng)默認(rèn)的類加載器不能正常工作。在JVM規(guī)范和JDK文檔中(1.2或者以后版本中),都沒有建議用戶覆寫loadClass(…)方法,相比而言,明確提示開發(fā)者在開發(fā)自定義的類加載器時(shí)覆寫findClass(…)邏輯。舉一個(gè)例子來驗(yàn)證該問題:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//用戶自定義類加載器WrongClassLoader.Java(覆寫loadClass邏輯) publicclassWrongClassLoaderextends ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { returnthis.findClass(name); } protected Class<?> findClass(String name) throws ClassNotFoundException { //假設(shè)此處只是到工程以外的特定目錄D:/library下去加載類 具體實(shí)現(xiàn)代碼省略 } } |
通過前面的分析我們已經(jīng)知道,用戶自定義類加載器(WrongClassLoader)的默
認(rèn)的類加載器是系統(tǒng)類加載器,但是現(xiàn)在問題4種的結(jié)論就不成立了。大家可以簡(jiǎn)
單測(cè)試一下,現(xiàn)在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工
程類路徑上的類都加載不上了。
問題5測(cè)試代碼一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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物理存在的)
輸出結(jié)果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系統(tǒng)找不到指定的路徑。) 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(…)引起的邏輯錯(cuò)誤明顯是比較簡(jiǎn)單的,實(shí)際引起的邏輯錯(cuò)誤可能復(fù)雜的多。
問題5測(cè)試二
1
2
3
4
5
6
7
8
9
10
11
12
|
//用戶自定義類加載器WrongClassLoader.Java(不覆寫loadClass邏輯) publicclassWrongClassLoaderextends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { //假設(shè)此處只是到工程以外的特定目錄D:/library下去加載類 具體實(shí)現(xiàn)代碼省略 } } |
將自定義類加載器代碼WrongClassLoader.Java做以上修改后,再運(yùn)行測(cè)試代碼,輸出結(jié)果如下:
1
2
3
|
beans.Account WrongClassLoader@1c78e57 |
這說明,beans.Account加載成功,且是由自定義類加載器WrongClassLoader加載。
這其中的原因分析,我想這里就不必解釋了,大家應(yīng)該可以分析的出來了。
(2)正確設(shè)置父類加載器
通過上面問題4和問題5的分析我們應(yīng)該已經(jīng)理解,個(gè)人覺得這是自定義用戶類加載器時(shí)最重要的一點(diǎn),但常常被忽略或者輕易帶過。有了前面JDK代碼的分析作為基礎(chǔ),我想現(xiàn)在大家都可以隨便舉出例子了。
(3)保證findClass(String )方法的邏輯正確性
事先盡量準(zhǔn)確理解待定義的類加載器要完成的加載任務(wù),確保最大程度上能夠獲取到對(duì)應(yīng)的字節(jié)碼內(nèi)容。
6.如何在運(yùn)行時(shí)判斷系統(tǒng)類加載器能加載哪些路徑下的類?
一是可以直接調(diào)用ClassLoader.getSystemClassLoader()或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴(kuò)展類加載器本身都派生自URLClassLoader),調(diào)用URLClassLoader中的getURLs()方法可以獲取到;
二是可以直接通過獲取系統(tǒng)屬性java.class.path 來查看當(dāng)前類路徑上的條目信息 , System.getProperty("java.class.path")
7.如何在運(yùn)行時(shí)判斷標(biāo)準(zhǔn)擴(kuò)展類加載器能加載哪些路徑下的類?
方法之一:
1
2
3
4
5
6
7
8
9
10
|
try { URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs(); for ( int i = 0 ; i < extURLs.length; i++) { System.out.println(extURLs[i]); } } catch (Exception e) { //…} |
本機(jī)對(duì)應(yīng)輸出如下:
1
2
3
4
5
6
7
|
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 |