一文讀懂系列-ClassLoader
我們都知道JVM中所有的類都是通過類加載器ClassLoader加載到JVM中才可以使用,本文就介紹下ClassLoader,讓大家從底層知道一個類是如何加載出來的,當再遇到ClassNotFound Error的時候知道該怎么查問題。
類加載
類加載過程JVM需要完成3個工作:
1)通過全路徑類名來獲取該類的二進制字節(jié)流;
2)將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
3)在java堆中生成一個代表這個類的Class對象,作為方法區(qū)訪問這些數(shù)據(jù)的訪問入口。
類加載過程中需要的類的二進制字節(jié)流,可以有多種形式提供,可以是最常見的class文件,也可以是網(wǎng)絡(luò)上獲取到的二進制字節(jié)流,當然也可以是jar包、war包之類的class文件壓縮包,這樣的設(shè)定就給了開發(fā)者很大的空間,可以利用這樣的特性來自由的實現(xiàn)類的動態(tài)下發(fā)、動態(tài)更新加載類,實現(xiàn)功能的動態(tài)發(fā)布和功能的動態(tài)更新??梢园l(fā)現(xiàn)我們?nèi)粘S玫降腡omcat、Jboss、WAS等這類應(yīng)用服務(wù)器都可以實現(xiàn)不停服務(wù)動態(tài)增量更新版本,這些功能都是通過類加載器動態(tài)加載class文件來實現(xiàn)的,包括OSGi這樣的模塊化框架都是通過類加載器完成的。
加載階段完成后,虛擬機將外部的二進制字節(jié)流按照JVM的要求將類數(shù)據(jù)存儲在方法區(qū)中,然后在Java堆中實例化一個類的對象,這個對象將作為程序訪問方法區(qū)的類型數(shù)據(jù)的入口。
類加載器
介紹了類加載的過程類被加載到JVM中需要有類加載器,下面介紹下類加載器,類加載器可以分為三種:
1)Bootstrap ClassLoader 啟動類加載器:啟動類加載器負責將JAVA_HOME/lib下的類庫加載到虛擬機中;
2)Extension ClassLoader 擴展類加載器:擴展類加載器負責將JAVA_HOME/lib/ext目錄下的類戶籍在到虛擬機中;
3)Application ClassLoader 應(yīng)用程序類加載器:負責加載用戶類路徑上所指定的類庫,開發(fā)者可以直接通過ClassLader的getSystemClassLoader()方法獲取到該類應(yīng)用程序的類加載器。
在JVM中判斷一個類的唯一性,除了要看類自身還要看該類的類加載器,必須類和加載該類的類加載器都一致才能說明該類在JVM中的唯一性。如果同一個類的二進制字節(jié)碼,被兩個不同的ClassLoader去加載那么這兩個類在同一個JVM中也是不同的存在,通過Class的equals()方法和instanceof 都可以進行判斷出這兩個類是不同的。
下面這個例子自定義了一個ClassLoader,并且通過這個類加載器加載了com.monkey01.jvm.ClassLoaderTest這個類,可以發(fā)現(xiàn)獲取的類全路徑名是com.monkey01.jvm.ClassLoaderTest,但是通過instanceof去比對類型卻是false,因為jvm啟動的時候系統(tǒng)通過AppClassLoader也加載了一個com.monkey01.jvm.ClassLoaderTest類,所以比對下來是不一樣的兩個類。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
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 (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.monkey01.jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.monkey01.jvm.ClassLoaderTest);
}
}
concole out:
class com.monkey01.jvm.ClassLoaderTest
false
雙親委派
類加載的過程中有個很核心的概念-雙親委派,雙親委派模型是JVM中類加載的一種父子模型,其實這種類似的模型在很多其它開發(fā)語言中也有類似的使用,例如android、iOS中的事件分發(fā)模型其實也是一種委派模型。JVM中類加載的雙親委派的流程是:如果一個類加載器收到了一個類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去加載類,每一個層次的類加載器都是這樣操作,因此所有的加載請求最終都會傳遞到頂層的啟動類加載器,只有當父加載器在指定的搜索范圍內(nèi)沒有找到需要加載的類反饋無法完成這個加載請求時,子加載器才會去自己加載,如果還是無法加載則繼續(xù)傳給孫加載器,就這樣一層層的進行傳遞,直到最終的葉子加載器還無法加載就會報ClassNotFound Exception。這里還要注意下每個加載器都會先在自己的緩存中查找是否已經(jīng)加載,如果已經(jīng)加載了就直接從緩存中返回,不需要再去加載class文件了。

使用雙親委派模型最大的優(yōu)點在于,所有的加載請求都是向上傳遞的,對于一些屬于系統(tǒng)的類,例如rt.jar中的一些類,不管是哪個類加載器去加載,最后都會由Bootstrap ClassLoader去加載完成,這樣就保證了系統(tǒng)類在同一個JVM環(huán)境中只有一個類,不會因為不同類加載器去加載而在JVM中產(chǎn)生多份。
類加載器加載源碼
最后我們通過查看源碼來了解下ClassLoader加載源碼的代碼實現(xiàn),ClassLoader所有的加載過程都是通過loadClass()方法來實現(xiàn)的,在loadClass中的邏輯分為下面3步:
1)執(zhí)行findLoadedClass(String)去檢測這個class是不是已經(jīng)加載過了。
2)執(zhí)行父加載器的loadClass方法。如果父加載器為null,則jvm內(nèi)置的加載器去替代,也就是Bootstrap ClassLoader。這也解釋了ExtClassLoader的parent為null,但仍然說Bootstrap ClassLoader是它的父加載器。
3)如果向上委托父加載器沒有加載成功,則通過findClass(String)查找。
如果class在上面的步驟中找到了,參數(shù)resolve又是true的話,那么loadClass()又會調(diào)用resolveClass(Class)這個方法來生成最終的Class對象。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,檢測是否已經(jīng)加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加載器不為空則調(diào)用父加載器的loadClass
c = parent.loadClass(name, false);
} else {
//父加載器為空則調(diào)用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} 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();
//父加載器沒有找到,則調(diào)用findclass
c = findClass(name);
// 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) {
//調(diào)用resolveClass()
resolveClass(c);
}
return c;
}
}
總結(jié)
從整篇文章我們可以了解到JVM中ClassLoader的作用、分類、雙親委派、類加載過程,讓大家能從更加全面的角度了解ClassLoader,并不是簡單停留在使用的層面,讓大家從底層更加深刻的認識ClassLoader。