雖然前面把class文件的產(chǎn)生到加載使用流程說(shuō)了一遍,但是還是想具體看看classLoader的雙親委托具體是如何運(yùn)行的,有什么利弊。
還有想看看不同類加載器的不同命名空間帶來(lái)那些好處和實(shí)際有那些應(yīng)用?并且想對(duì)ClassLoader加載類這個(gè)過(guò)程進(jìn)行更加底層的了解,通過(guò)閱讀源代碼和自定義類加載器方式實(shí)踐。
雙親委托機(jī)制?
還是先看看JVM中的類加載器層次結(jié)構(gòu)如下:
Bootstrap classLoader
/\
/||\
Extenssion ClassLoader
/\
/||\
Application ClassLoader
/| |\
User ClassLoader User ClassLoader(自定義類加載器)
我們應(yīng)用程序中的所有類,都是這幾種類加載器加載的。當(dāng)JVM從某個(gè)二進(jìn)制字節(jié)流中讀取類的時(shí)候,具體這幾個(gè)類加載器是如何工作的呢?它們之間除了看上去的父子層次關(guān)系,在具體的加載類的時(shí)候,又有怎樣的關(guān)系?
什么是雙親委托,原理機(jī)制是什么?
JVM中的classLoader在搜索某個(gè)類的時(shí)候,是使用雙親委托模型機(jī)制工作的。
該模型的前提條件就是:除了JVM自帶的BootstrapClassLoader引導(dǎo)類加載器外,其他的類加載器都必須屬于自己的父加載器(不是以繼承方式實(shí)現(xiàn)父子關(guān)系,而是使用組合包含關(guān)系)。所以,這種父子層級(jí)之間的關(guān)系,就將某個(gè)類加載過(guò)程給規(guī)定好了。
我們也可以從java.lang.ClassLoader.class中看到這種組合模式形成的類加載器父子層級(jí)關(guān)系:
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent; //父類加載器s
...
}
所以,基于這種組合模式形成的類加載器父子層級(jí)關(guān)系背景下,雙親委托其工作過(guò)程:(假設(shè)C加載器是最頂層的Bootstrap引導(dǎo)類加載器)
如果一個(gè)類加載器(classLoader)A收到了加載類的請(qǐng)求,那么A首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)”請(qǐng)求”委托給A的父加載器B去完成,調(diào)用getParent()方法可以得到自己的父加載(若方法返回null表示該加載器為引導(dǎo)類加載器)。加載器B也會(huì)把該請(qǐng)求委派給自己的父加載器C。在加載該類的每一層都會(huì)進(jìn)行如此父類處理委托。
因此所有的加載請(qǐng)求最終都會(huì)發(fā)送到頂層的啟動(dòng)類加載器(BootStrap ClassLoader)中,只有當(dāng)父加載器反饋無(wú)法完成這個(gè)加載請(qǐng)求(在它的搜索范圍i額沒(méi)有找到所需要的類),子加載器才會(huì)嘗試自己去加載。
eg:
請(qǐng)求加載類tclass,根據(jù)當(dāng)先系統(tǒng)上下文中得到當(dāng)前的類加載器A,判斷A是否有父加載器,若是有,則將該請(qǐng)求發(fā)送給父加載器B,在請(qǐng)求發(fā)送給C。
若是C無(wú)法加載該tclass(Bootstrap加載/lib下的類),就讓B加載,若是B成功加載則完成該類tclass的加載,成功生成對(duì)應(yīng)的java.lang.Class實(shí)例,A不需要在進(jìn)行加載。若是B也無(wú)法加載,則最后交給A加載。
若是A加載成功,返回Class實(shí)例,若是加載失敗,拋出class相關(guān)的異常,程序結(jié)束。
同時(shí),可以查看JDK的源代碼中是如何表達(dá)該過(guò)程的:
java.lang.ClassLoader中對(duì)于該方法的解釋是這樣的:(英文翻譯有點(diǎn)渣哈,╮(╯▽╰)╭…)
若是要加載name指定的二進(jìn)制字節(jié)碼流文件,類加載器默認(rèn)會(huì)按照以下步驟來(lái)搜索類:
a. 調(diào)用findLoadedClass方法來(lái)檢查該類是否已經(jīng)被類加載器加載了。
b. 如果當(dāng)前上下文的類加載器的父加載器不為空,則調(diào)用父加載器的loadClass方法來(lái)查找類;若是沒(méi)有父加載器,則使用JVM內(nèi)建的Bootstrap加載器來(lái)查找類。
c.最后,才是調(diào)用findClass方法去查找該類。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//A. First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //B.有父加載器,則委托父加載器調(diào)用loadClass方法查找加載類
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name); //B.無(wú)父加載器,則委托JVM內(nèi)建啟動(dòng)類加載器加載該類
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //C.若是所有父加載器都無(wú)法加載該類字節(jié)碼,則調(diào)用findClass方法去查找類。
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我們可以看到第三步驟C中,當(dāng)所有父加載器都無(wú)法加載該類的時(shí)候,需要調(diào)用findClass方法。
而源代碼中該類僅僅只有一個(gè)protected訪問(wèn)權(quán)限的方法聲明,根本沒(méi)有實(shí)現(xiàn)內(nèi)容,側(cè)面就說(shuō)明了,這個(gè)方法是提供給開(kāi)發(fā)人員自定義拓展用的。
用于定義自定義的ClassLoader,覆蓋該findClass方法。那么當(dāng)在程序運(yùn)行中,在雙親委托無(wú)法加載該類的最后一步,就是需要使用我們自定義的類加載器調(diào)用自定義的findClass來(lái)覆蓋如何實(shí)現(xiàn)加載該類的細(xì)節(jié)。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
具體如何自定義類加載器,在后面會(huì)說(shuō)啦。。下圖是雙親委托模型的流程圖:

為什么要使用這種雙親委托模型?
使用雙親委托模型好處有以下幾點(diǎn):
可以避免java類的重復(fù)加載,當(dāng)父類加載器已經(jīng)加載了,那么子加載器就沒(méi)有必要重新加載一遍。而java類的雙親委托模型的搜索過(guò)程總比類加載器再加載一次耗時(shí)少,節(jié)省資源把。
可以避免用戶自定義同路徑類對(duì)于J2SE平臺(tái)自己定義的核心API的破壞。例如java.lang.Object類,它存放在rt.jar包中,無(wú)論哪一個(gè)類加載器需要加載這個(gè)類,最終都是委派給處于最頂端的啟動(dòng)類加載器(Bootstrap ClassLoader)進(jìn)行加載,因此,Object類在所有程序各種類加載器環(huán)境中(也相當(dāng)于類加載器命名空間中)都是同一個(gè)類(沒(méi)辦法,因?yàn)橐龑?dǎo)類加載器是JVM啟動(dòng)自帶首先啟動(dòng)的,是第一次加載所有基礎(chǔ)類庫(kù)的類加載器)。
相反,若是不使用雙親委托機(jī)制,由各個(gè)類加載器去自行加載的話,如果用戶自定義了一個(gè)java.lang.Object類,放在Classpath中,那么系統(tǒng)將會(huì)出現(xiàn)多個(gè)不同的Object類,導(dǎo)致java類型體系中最基礎(chǔ)的行為也就無(wú)法保證正常運(yùn)行。雙親委托模型保證了java程序運(yùn)行的穩(wěn)定。基于JVM標(biāo)識(shí)每個(gè)類的唯一性需要與類加載器一同來(lái)判斷,那么,通過(guò)我們自定義的類加載器加載的類,就能很靈活和方便的與其他甚至同名的類區(qū)分開(kāi)來(lái),進(jìn)行隔離使用。大大增強(qiáng)了我們對(duì)類的使用。
這種委托機(jī)制的利弊,如何理解雙親委派模型的被破化?
JVM規(guī)范中,雙親委托模型并不是一個(gè)強(qiáng)制性的約束,而是java設(shè)計(jì)者推薦給開(kāi)發(fā)者的類加載實(shí)現(xiàn)方式。在java的世界中大部分的類加載器都遵循這個(gè)模型。但到目前為止,該模型主要出現(xiàn)以下幾次大規(guī)模”被破壞”的情況:(參考《深入理解JVM虛擬機(jī)》)
a. 雙親委托模型是在JDK1.2之后出現(xiàn)的。在此之前類加載器和抽象類java.lang.ClassLoader就已經(jīng)存在了。所以為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個(gè)新的protected方法findClass(),(在上面的源代碼中也可以看見(jiàn))。
而在雙親委托模型未設(shè)計(jì)出前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫(xiě)loadClass()方法,因?yàn)镴VM在進(jìn)行類加載的時(shí)候,會(huì)調(diào)用加載器私有方法loadClassInternal(),而這個(gè)方法的唯一邏輯就是去調(diào)用自己的loadClass()方法。
參考源碼(java.lang.ClassLoader.java)
私有只是用于虛擬機(jī)調(diào)用來(lái)加載類的入口:
// This method is invoked by the virtual machine to load a class.
private Class loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
而在JDK1.2雙親委托出現(xiàn)之后,不提倡用戶再去覆蓋loadClass()方法,而應(yīng)當(dāng)把自己的類加載邏輯寫(xiě)到findClass()方法完成加載,在loadClass()方法父類加載失敗,則會(huì)調(diào)用自己的findClass方法完成加載,這樣就保證寫(xiě)出來(lái)的類加載器是符合雙親委托規(guī)則的。
b. 二次破環(huán)是該雙親委托模弊端引起的。該模型能很好的解決各個(gè)類加載器對(duì)基礎(chǔ)類的統(tǒng)一問(wèn)題。因?yàn)樗鼈兛偸亲鳛楸挥脩舸a調(diào)用的API,但是問(wèn)題就來(lái)了。如果基礎(chǔ)類又要調(diào)用回用戶的代碼,該如何實(shí)現(xiàn)?。
典型例子:JNDI服務(wù),它的代碼由啟動(dòng)類加載器加載(在rt.jar中),但JNDI目的就是對(duì)整個(gè)程序的資源進(jìn)行幾種管理和查找,需要調(diào)用由每個(gè)不同獨(dú)立廠商實(shí)現(xiàn)并且部署在應(yīng)用程序的ClassPath下的JNDI接口提供者的代碼。但是在應(yīng)用啟動(dòng)時(shí)候讀取rt.jar包時(shí)候,是不認(rèn)識(shí)這些三方廠商定義的類的,那么如何解決?
java設(shè)計(jì)團(tuán)隊(duì)引入了一個(gè)新設(shè)計(jì):線程上下文類加載器(Thread Context ClassLoader)。這個(gè)類加載器可以通過(guò)java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時(shí)候,還未設(shè)置,將會(huì)從父線程中繼承一個(gè)。如果在應(yīng)用程序全局范圍都沒(méi)有設(shè)置,默認(rèn)是appClassLoader類加載器。
class Thread implements Runnable {
...
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
@CallerSensitive
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
//
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
public static native Thread currentThread(); //用于獲取當(dāng)前現(xiàn)在運(yùn)行程序的線程
...
}
如何自定義類加載器
那么我們?nèi)绾螌?shí)現(xiàn)自定義類加載呢,其實(shí)很簡(jiǎn)單,只要繼承抽象的java.lang.ClassLoader類即可。然后重寫(xiě)findClass()方法,實(shí)現(xiàn)自己類加載器加載類的具體規(guī)則和實(shí)現(xiàn)。那我們?yōu)槭裁催€要自己定義類加載器呢?好處多啊,在上文也說(shuō)到了,具體下文說(shuō)到。
定義自已的類加載器分為兩步:(其實(shí)也可以重寫(xiě)loadClass方法的,不過(guò)也就破化了雙親委托模型)
- 繼承java.lang.ClassLoader
- 重寫(xiě)父類的findClass方法
父類有那么多方法,為什么偏偏只重寫(xiě)findClass方法?
因?yàn)镴DK已經(jīng)在loadClass方法中幫我們實(shí)現(xiàn)了ClassLoader搜索類的算法,當(dāng)在loadClass方法中搜索不到類時(shí),loadClass方法就會(huì)調(diào)用findClass方法來(lái)搜索類,所以我們只需重寫(xiě)該方法即可。
來(lái)具體實(shí)現(xiàn)一個(gè)實(shí)踐實(shí)踐看看:
- 在c盤(pán)桌面放置clazz文件夾,里面由ClassTest.class文件。(可以加載本地,網(wǎng)絡(luò)上的,數(shù)據(jù)庫(kù)等等位置的類文件)
public class ClassLoaderTest extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = getClassFile(name);
try {
byte[] bytes = getClassBytes(file);
return defineClass(name,bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private File getClassFile(String name) {
// return new File(name);
return new File("C:\\Users\\XianSky\\Desktop\\clazz\\ClassTest.class");
}
private static byte[] getClassBytes(File file) throws Exception {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream aos = new ByteArrayOutputStream(fis.available());
byte[] bytes = new byte[fis.available()]; //使用fis.avaliable()方法確保整個(gè)字節(jié)數(shù)組沒(méi)有多余數(shù)據(jù)
fis.read(bytes);
aos.write(bytes);
fis.close();
return aos.toByteArray();
}
public static void main(String[] args) throws Exception {
ClassLoaderTest ct = new ClassLoaderTest();
Class c = Class.forName("clazz.ClassTest", true, ct);
System.out.println(c.getClassLoader());
}
}
//輸出
clazz.ClassLoaderTest@15db9742
可以看到獲取本地file路徑下的class文件的二進(jìn)制字節(jié)碼,然后使用自定義的類加載器進(jìn)行加載,輸出的就是自定義的類加載器。這種形式的類字節(jié)碼加載過(guò)程很顯然易見(jiàn),是因?yàn)楫?dāng)前類加載器的所有父類加載器都查找不到該類字節(jié)流,所以最后是使用我們自定義的類加載實(shí)現(xiàn)加載的。
2.可以使用findClass方法加載同樣環(huán)境下的同一個(gè)類文件,也能達(dá)到不能類加載加載相同類也是不同的結(jié)果。
在eclipse中項(xiàng)目中存在以下目錄結(jié)構(gòu)文件,與上述A不同的是,我們自定義的加載器的父類加載器AppClassLoader其實(shí)是可以加載到clazz.ClassTest類的,根據(jù)雙親委托加載想,那父類都能加載了,為什么第一個(gè)clazz.ClassTest的類加載器還是我們自定義的ClassLoaderTest呢?
--projectName
-src
-clazz
-ClassLoaderTest.java
-ClassTest.java
我們也可以加載同一個(gè)路徑下的java類,重點(diǎn)看看程序運(yùn)行輸出的對(duì)象示例的內(nèi)存地址。(當(dāng)前開(kāi)發(fā)工具eclipse編譯后的類字節(jié)通過(guò)appClassLoader是可以加載得到的)
public class ClassLoaderTest extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// File file = getClassFile(name);
// try {
// byte[] bytes = getClassBytes(file);
// return defineClass(name,bytes, 0, bytes.length);
// } catch (Exception e) {
// e.printStackTrace();
// }
// return super.findClass(name);
try{
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is ==null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch (Exception e){
throw new ClassNotFoundException(name);
}
}
private File getClassFile(String name) {
// return new File(name);
return new File(name);
}
private static byte[] getClassBytes(File file) throws Exception {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream aos = new ByteArrayOutputStream(fis.available());
byte[] bytes = new byte[fis.available()]; //使用fis.avaliable()方法確保整個(gè)字節(jié)數(shù)組沒(méi)有多余數(shù)據(jù)
fis.read(bytes);
aos.write(bytes);
fis.close();
return aos.toByteArray();
}
public static void main(String[] args) throws Exception {
ClassLoaderTest ct = new ClassLoaderTest();
Object obj = ct.findClass("clazz.ClassTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
Class c = Class.forName("clazz.ClassTest", true, ct);
System.out.println(c.getClassLoader());
System.out.println(clazz.ClassLoaderTest.class.getClassLoader());
}
}
//輸出
class clazz.ClassTest
clazz.ClassLoaderTest@6d06d69c
clazz.ClassLoaderTest@6d06d69c
sun.misc.Launcher$AppClassLoader@73d16e93
可以看到,第一次使用自定義類加載findClass加載的class.ClassTest是我們自定義加載器的加載,最后那個(gè)clazz.ClassLoaderTest.class是appClassLoader加載,這就說(shuō)明了我們自定義的類加載的父類加載器是可以訪問(wèn)查找到這個(gè)類,至于為什么會(huì)這樣,我就根據(jù)自己的理解將加載class.ClassTest類時(shí)序圖過(guò)程畫(huà)出來(lái):

這里要注意的一個(gè)是:
在雙親委托過(guò)程中,真正完成類加載工作的類加載器和啟動(dòng)這個(gè)加載過(guò)程的類加載器,可能不是同一個(gè)。真正完成類加載工作是通過(guò)defineClass來(lái)實(shí)現(xiàn)的;而啟動(dòng)類的加載過(guò)程是通過(guò)調(diào)用loadClass來(lái)實(shí)現(xiàn)的。前者稱為一個(gè)類的定義加載器(defining loader),后者稱為初始加載器(initating loader)。在JVM中,那個(gè)類加載器啟動(dòng)類的加載過(guò)程并不重要,重要的是最終定義這個(gè)類的加載器。
所以這里的clazz.ClassTest中,引導(dǎo)類加載器相當(dāng)于類的初始加載器,而自定義的類加載是定義加載器。所以clazz.ClassTest.class.getClassLoader方法返回的是我們自定義的類加載。(這也僅僅是我自己的理解,僅供參考,也有可能不對(duì)啊,還需努力…)
- 通過(guò)重寫(xiě)loadClass方法來(lái)自定義類加載器
public class ClassLoaderTest2 {
public static void main(String[] args) throws Exception{
//通過(guò)重寫(xiě)loadClass方法來(lái)自定義類加載器
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is ==null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
}catch (Exception e){
throw new ClassNotFoundException(name);
}
};
};
Object obj = myLoader.loadClass("clazz.ClassLoaderTest2").newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass().getClassLoader());
System.out.println(clazz.ClassLoaderTest2.class.getClassLoader());
System.out.println(obj instanceof clazz.ClassLoaderTest2);
}
//output:
class clazz.ClassLoaderTest2
clazz.ClassLoaderTest2$1@7852e922
sun.misc.Launcher$AppClassLoader@73d16e93
false
}
可以看見(jiàn),不同的類加載器加載相同的一個(gè)類,會(huì)對(duì)instanceof造成一定的影響。雖然存在了兩個(gè)一樣的ClassLoaderTest2,但是是兩個(gè)不同類加載器加載的。一個(gè)是應(yīng)用類加載器加載,另外一個(gè)是我們自定義類加載器加載的,所以是不同的兩個(gè)類。
類加載器與類標(biāo)識(shí)的命名空間?
也就是說(shuō),在JVM搜索類的時(shí)候,如何判斷兩個(gè)相同全限定名的類是否是同一個(gè)類?也就會(huì)對(duì)后面判斷該類是否已經(jīng)加載的流程有重要影響了。
JVM在判定兩個(gè)class是否相同時(shí)候,不僅要判斷兩個(gè)類是否相同(equals,hashCode,全限定名..),還要判斷該類是否由同一個(gè)類加載器實(shí)例加載的。只有兩者同時(shí)滿足的情況下,JVM才會(huì)認(rèn)為這兩個(gè)class是相同。
關(guān)于類加載器和類共同標(biāo)識(shí)命名空間和類加載的共享和隔離問(wèn)題,可以拿tomcat來(lái)作為例子說(shuō)明。tomcat內(nèi)部由自己定義的類加載器,還可以通過(guò)tomcat安裝目錄下的catalina.properties文件靈活配置應(yīng)用共享類庫(kù),和應(yīng)用工之間單獨(dú)使用的類庫(kù)。這個(gè),就放在下次說(shuō)了,我也好好看看,思考思考。。。
參考: [深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐].周志明
深入探討 Java 類加載器-成富