無論你是跟同事、同學(xué)、上下級、同行、或者面試官討論技術(shù)問題的時(shí)候,很容易卷入JVM大型撕逼現(xiàn)場。為了能夠讓大家從大型撕逼現(xiàn)場中脫穎而出,最近我苦思冥想如何把知識點(diǎn)盡可能呈現(xiàn)的容易理解,方便記憶。于是就開啟了這一系列文章的編寫。為了讓JVM相關(guān)知識點(diǎn)能夠形成一個(gè)體系,arthinking將編寫整理一系列的專題,以盡量以圖片的方式描述相關(guān)知識點(diǎn),并且最終把所有相關(guān)知識點(diǎn)串成了一張圖。持續(xù)更新中,歡迎大家閱讀。有任何錯(cuò)落之處也請您高抬貴手幫忙指正,感謝!
導(dǎo)讀:
- 類加載器是怎么被創(chuàng)建出來的?
- 什么是雙親委派機(jī)制?為什么要有這種機(jī)制?
- Class實(shí)例和類加載器究竟是在Java Heap中,還是在方法區(qū)中?
類加載器: 可以實(shí)現(xiàn)通過一個(gè)類的全限定名稱來獲取描述此類的二進(jìn)制字節(jié)流。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊成為”類加載器“。
通過自定義類加載器可以實(shí)現(xiàn)各種有趣而強(qiáng)大的功能更:OSGi,熱部署,代碼加密等。
1、類加載器的加載流程
如上圖為類加載器的加載流程。
這里簡單描述下:
1.1、啟動(dòng)類加載器
啟動(dòng)類加載器:系統(tǒng)啟動(dòng)的時(shí)候,首先會(huì)通過由C++實(shí)現(xiàn)的啟動(dòng)類加載器,加載<JAVA_HOME>/lib目錄下面的jar包,或者被-Xbootclasspath參數(shù)指定的路徑并且被虛擬機(jī)識別的文件名的jar包。把相關(guān)Class加載到方法區(qū)中。
這一步會(huì)加載關(guān)鍵的一個(gè)類:sun.misc.Launcher
。這個(gè)類包含了兩個(gè)靜態(tài)內(nèi)部類:
- ExtClassLoader:擴(kuò)展類加載器內(nèi)部類,下面會(huì)講;
- AppClassLoader:應(yīng)用程序類加載器內(nèi)部類,下面會(huì)講
可以反編譯rt.jar文件查看詳細(xì)代碼:
image-20200105124613663image-20200105131342939
在加載到Launcher類完成后,會(huì)對該類進(jìn)行初始化,初始化的過程中,會(huì)創(chuàng)建 ExtClassLoader 和 AppClassLoader,源碼如下:
public Launcher() {
ExtClassLoader extClassLoader;
try {
extClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException iOException) {
throw new InternalError("Could not create extension class loader", iOException);
}
try {
this.loader = AppClassLoader.getAppClassLoader(extClassLoader);
} catch (IOException iOException) {
throw new InternalError("Could not create application class loader", iOException);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
由于啟動(dòng)類加載器是由C++實(shí)現(xiàn)的,所以在Java代碼里面是訪問不到啟動(dòng)類加載器的,如果嘗試通過String.class.getClassLoader()
獲取啟動(dòng)類的引用,會(huì)返回null
;
問題:
啟動(dòng)類加載器,擴(kuò)展類加載器和應(yīng)用類加載器都是又誰加載的?
- 啟動(dòng)類加載器是JVM的內(nèi)部實(shí)現(xiàn),在JVM申請好內(nèi)存之后,由JVM創(chuàng)建這個(gè)啟動(dòng)類加載器
- 擴(kuò)展類加載器和應(yīng)用程序類加載器是由啟動(dòng)類加載器加載進(jìn)來的;
說說以下代碼輸出什么:
public static void main(String[] args) { System.out.println("加載當(dāng)前類的加載器:" + TestClassLoader.class.getClassLoader()); System.out.println("加載應(yīng)用程序類加載器的加載器" + TestClassLoader.class.getClassLoader().getClass().getClassLoader()); System.out.println("String類的啟動(dòng)類加載器" + String.class.getClassLoader()); }
1.2、擴(kuò)展類加載器
如上圖,擴(kuò)展類加載器負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者被java.ext.dirs系統(tǒng)變量指定的路徑中的類。
1.3、應(yīng)用程序類加載器
引用程序類加載器加載用戶類路徑下制定的類庫,如果應(yīng)用程序沒有自定義過自己的類加載器,此類加載器就是默認(rèn)的類加載器。
引用程序類加載器也叫系統(tǒng)類加載器,可以通過getSystemClassLoader
方法得到應(yīng)用程序類加載器。
注意,如上圖通過以上三個(gè)類加載器加載類到方法區(qū)之后,方法區(qū)中分別對應(yīng)有各自的類信息存儲(chǔ)區(qū)。不同類加載器加載的同一個(gè)類文件不相等。
2、類加載器的雙親委派機(jī)制
2.1、雙親委派機(jī)制原理
雙親委派模型在JDK1.2之后被引入,并廣泛使用,這不是一個(gè)強(qiáng)制性的約束模型,二貨思Java設(shè)計(jì)者推薦給開發(fā)者的一種類加載器實(shí)現(xiàn)方式。我們也可以覆蓋對應(yīng)的方式,實(shí)現(xiàn)自己的加載模型。
類加載器的雙親委派機(jī)制如下:
也就是說:
- 一個(gè)類加載器收到了類加載請求,不會(huì)自己立刻嘗試加載類,而是把請求委托給父加載器去完成,每一層都是如此,所有的家在請求最終都傳遞到最頂層的類加載器進(jìn)行處理;
- 如果父加載器不存在了,那么嘗試判斷有沒有被啟動(dòng)類加載器加載;
- 如果的確沒有被夾在,則再自己嘗試加載。
問題:
- 為什么要有這么復(fù)雜的雙親委派機(jī)制?
- 如果沒有這種機(jī)制,我們就可以篡改啟動(dòng)類加載器中需要的類了,如,修自己編寫一個(gè)
java.lang.Object
用自己的類加載器進(jìn)行加載,系統(tǒng)中就會(huì)存在多個(gè)Object類,這樣Java類型體系最基本的行為也就無法保證了。
2.2、雙親委派機(jī)制處理流程
JDK中默認(rèn)的雙親委派處理流程是怎么的呢?接下來我們看看代碼,以下是java.lang.ClassLoader.loadClass()
方法的實(shí)現(xiàn):
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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();
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) {
resolveClass(c);
}
return c;
}
}
轉(zhuǎn)成流程圖,即是:
如山圖所以,總是先回嘗試讓父類加載器先加載,其次判斷啟動(dòng)類加載器是否已經(jīng)加載了,最后才嘗試從當(dāng)前類加載器加載。轉(zhuǎn)換為更清晰的模型如下:
雙親委派模型具有以下特點(diǎn):
- 可見性原則:
- 應(yīng)用類加載器是可以讀取到由擴(kuò)展類加載器和啟動(dòng)類加載器加載進(jìn)來的Class的;
- 擴(kuò)展類加載器是可以讀取到由啟動(dòng)類加載器加載進(jìn)來的Class的;
- 唯一性:
- 類是唯一的,沒有重復(fù)的類;
2.3、類加載器和Class實(shí)例的題外話
啟動(dòng)類加載器,擴(kuò)展類加載器,應(yīng)用程序類加載器,他們分別管理者各自方法區(qū)里的一個(gè)區(qū)塊。
根據(jù)上一篇文章我們知道,方法區(qū)里面主要存儲(chǔ)的是類的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),這個(gè)類的在方法區(qū)中的各種數(shù)據(jù)結(jié)構(gòu)信息通過類的Class實(shí)例進(jìn)行訪問。
如下圖:
方法區(qū)里面存儲(chǔ)著加載進(jìn)來的類信息,方法區(qū)同時(shí)雇傭了兩類工種幫忙干活:
- 類加載器:負(fù)責(zé)管理各個(gè)存儲(chǔ)區(qū)的類信息,如加載和卸載類信息;
- Class實(shí)例:負(fù)責(zé)對接外部需求,如果外部有人想查看里面的類信息,則需要通過Class實(shí)例來獲取;
另外,方法區(qū)里面,啟動(dòng)類加載器類信息對擴(kuò)展兩類加載器類信息可見,而前面兩者的類信息又對應(yīng)用程序類加載器類信息可見。
3、其他非雙親委派模型的案例
3.1、JDK 1.0遺留問題
在JDK1.0已經(jīng)存在了ClassLoader類,但是當(dāng)時(shí)還沒有雙親委派機(jī)制,用戶為了自定義類加載器,需要重新loadClass()
方法,而我們知道,在JDK1.2以后,loadClass里面就是雙親委派機(jī)制的實(shí)現(xiàn)代碼,此時(shí),要實(shí)現(xiàn)自定義類加載器,需要重新findClass()類即可。
如果重新了loadClass()方法,也就意味著不再遵循雙親委派模型了。
3.2、線程上下文類加載器
為什么需要這個(gè)東西呢,我們還是從一個(gè)案例來說起。
Tomcat中的類加載器
我們知道Tomcat目錄結(jié)構(gòu)中有以下目錄:
/common/: 該目錄下的類庫可被Tomcat和所有的WebApp共同使用;
/server/: 該目錄下的類庫可被Tomcat使用,但對所有的WebApp不可見;
/shared/: 該目錄下的類庫可被所有的WebApp共同使用,但對Tomcat自己不可見;
另外Web應(yīng)用程序還有自身的類庫,放在/WebApp/WEB-INF
目錄中:這里面的類庫僅僅可以被此Web應(yīng)用程序使用,對Tomcat和其他Web應(yīng)用程序都不可見。
為了實(shí)現(xiàn)以上各個(gè)目錄的類庫可見性效果,Tomat提供了如下的自定義類加載器:
現(xiàn)在如下場景:
我們發(fā)現(xiàn)Tomcat下面有若干個(gè)webapp,每個(gè)webapp都用到了spring,于是我們把spring的jar包放到了shared
目錄中。
于是問題出現(xiàn)了:由于spring的jar包是由Shared類加載器加載的,假設(shè)我們要使用SpringContext的getBean方法,獲取webapp中的Bean,如果是按照雙親委派模型,就會(huì)有問題了,因?yàn)閣ebapp中的Java類是對SharedClassLoader不可見的:
Spring中的線程上下文類加載器
為了解決這個(gè)問題,Spring使用了線程上下文類加載器,即從ThreadLocal中獲取到當(dāng)前線程的上下文類加載器,來加載所有的類庫和類。
關(guān)于Spring初始化源碼相關(guān)解讀,參考我的這邊文章:Spring IoC原理剖析。
Spring中的bean類加載器
ApplicationContext中有一個(gè)beanClassLoader
字段,這個(gè)是bean的類加載器,在prepareBeanFactory()方法中做了初始化:
beanFactory.setBeanClassLoader(getClassLoader());
getClassLoader方法如下:
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
ClassUtils.getDefaultClassLoader()方法:
@Nullable
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
}
可以發(fā)現(xiàn),這里最終取了當(dāng)前線程上下文中的ClassLoader。
加載Bean
我們來看看Spring加載Class的代碼。這里我們直接找到實(shí)例化Singletons的方法跟進(jìn)去找需要關(guān)注的代碼:
我們發(fā)現(xiàn)在加載Bean Class的時(shí)候調(diào)用了這個(gè)方法:
AbstractBeanFactory:
ClassLoader beanClassLoader = getBeanClassLoader();
也就是用到了ApplicationContext中的beanClassLoader
,線程上下文類加載器來加載Bean Class實(shí)例。
總結(jié)
Spring作為一個(gè)第三方類庫,可能被任何的ClassLoader加載,所以最靈活的方式是直接使用上下文類加載器。
3.3、模塊熱部署
主要是類似OSGi這類的模塊化熱部署技術(shù)。在OSGi中不再是雙親委派模型中的樹狀結(jié)構(gòu),而是更復(fù)雜的網(wǎng)狀結(jié)構(gòu)。
References
Where are static methods and static variables stored in Java?
《深入理解Java虛擬機(jī)-JVM高級特性與最佳實(shí)踐》
Chapter 5. Loading, Linking, and Initializing
本文為arthinking
基于相關(guān)技術(shù)資料和官方文檔撰寫而成,確保內(nèi)容的準(zhǔn)確性,如果你發(fā)現(xiàn)了有何錯(cuò)漏之處,煩請高抬貴手幫忙指正,萬分感激。
大家可以關(guān)注我的博客:itzhai.com
獲取更多文章,我將持續(xù)更新后端相關(guān)技術(shù),涉及JVM、Java基礎(chǔ)、架構(gòu)設(shè)計(jì)、網(wǎng)絡(luò)編程、數(shù)據(jù)結(jié)構(gòu)、數(shù)據(jù)庫、算法、并發(fā)編程、分布式系統(tǒng)等相關(guān)內(nèi)容。
如果您覺得讀完本文有所收獲的話,可以關(guān)注
我的賬號,或者點(diǎn)贊
的,您的支持就是我寫作的動(dòng)力!關(guān)注我的公眾號,及時(shí)獲取最新的文章。
本文作者: arthinking
博客鏈接: https://www.itzhai.com/jvm/what-is-classloader-and-what-is-parents-delegation-model.html
版權(quán)聲明: 版權(quán)歸作者所有,未經(jīng)許可不得轉(zhuǎn)載,侵權(quán)必究!聯(lián)系作者請加公眾號。