面試必時必問的JVM 類加載機(jī)制詳解
前言
本次帶來 JVM 的另一塊重要內(nèi)容,類加載機(jī)制,不廢話,直接開懟。
正文
1、類加載的過程。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載7個階段。其中驗證、準(zhǔn)備、解析3個部分統(tǒng)稱為連接。
1)加載
“類加載”過程的一個階段,在加載階段,虛擬機(jī)需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
2)驗證
連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證。
3)準(zhǔn)備
該階段是正式為類變量(static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,下表列出了Java中所有基本數(shù)據(jù)類型的零值。
4)解析
該階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符這7類符號引用進(jìn)行。
5)初始化
到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源。
我們也可以從另外一種更直接的形式來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>() 不是程序員在 Java 代碼中直接編寫的方法,而是由 Javac 編譯器自動生成的。
<clinit>() 方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問。
2、Java 虛擬機(jī)中有哪些類加載器?
從 Java 虛擬機(jī)的角度來講,只存在兩種不同的類加載器:
一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn),是虛擬機(jī)自身的一部分;
另一種就是所有其他的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader。
從Java開發(fā)人員的角度來看,絕大部分Java程序都會使用到以下3種系統(tǒng)提供的類加載器。
1)啟動類加載器(Bootstrap ClassLoader):
這個類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。
2)擴(kuò)展類加載器(Extension ClassLoader):
這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴(kuò)展類加載器。
3)應(yīng)用程序類加載器(Application ClassLoader):
這個類加載器由sun.misc.Launcher$AppClassLoader實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
我們的應(yīng)用程序都是由這3種類加載器互相配合進(jìn)行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關(guān)系一般如圖所示。
3、什么是雙親委派模型?
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
類加載的源碼如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1、檢查請求的類是否已經(jīng)被加載過了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 2、將類加載請求先委托給父類加載器 if (parent != null) { // 父類加載器不為空時,委托給父類加載進(jìn)行加載 c = parent.loadClass(name, false); } else { // 父類加載器為空,則代表當(dāng)前是Bootstrap,從Bootstrap中加載類 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父類加載器拋出ClassNotFoundException // 說明父類加載器無法完成加載請求 } if (c == null) { // 3、在父類加載器無法加載的時候,再調(diào)用本身的findClass方法來進(jìn)行類加載 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; } }
4、為什么使用雙親委派模式?
1)使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處就是 Java 類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。
2)如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個java.lang.Object 的類,并放在程序的 ClassPath 中,那系統(tǒng)中將會出現(xiàn)多個不同的 Object 類,Java 類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂。
5、有哪些場景破壞了雙親委派模型?
目前比較常見的場景主要有:
1)線程上下文類加載器,典型的:JDBC 使用線程上下文類加載器加載 Driver 實現(xiàn)類
2)Tomcat 的多 Web 應(yīng)用程序
3)OSGI 實現(xiàn)模塊化熱部署
6、為什么要破壞雙親委派模型?
原因其實很簡單,就是使用雙親委派模型無法滿足需求了,因此只能破壞它,這邊以面試常問的 Tomcat 為例。
我們知道 Tomcat 容器可以同時部署多個 Web 應(yīng)用程序,多個 Web 應(yīng)用程序很容易存在依賴同一個 jar 包,但是版本不一樣的情況。例如應(yīng)用1和應(yīng)用2都依賴了 spring ,應(yīng)用1使用的 3.2.* 版本,而應(yīng)用2使用的是 4.3.* 版本。
如果遵循雙親委派模型,這個時候使用哪個版本了?
其實使用哪個版本都不行,很容易出現(xiàn)兼容性問題。因此,Tomcat 只能選擇破壞雙親委派模型。
7、如何破壞雙親委派模型?
破壞雙親委派模型的思路都比較類似,這邊以面試中常問到的 Tomcat 為例。
其實原理非常簡單,我們可以看到上面的類加載方法源碼(loadClass)的方法修飾符是 protected,因此我們只需以下幾步就能破壞雙親委派模型。
1)繼承 ClassLoader,Tomcat 中的 WebappClassLoader 繼承 ClassLoader 的子類 URLClassLoader。
2)重寫 loadClass 方法,實現(xiàn)自己的邏輯,不要每次都先委托給父類加載,例如可以先在本地加載,這樣就破壞了雙親委派模型了。
8、Tomcat 的類加載器?
Tomcat 的類加載器如下圖所示:
1)Bootstrap ClassLoader:可以看到上圖中缺少了 Extension ClassLoader,在 Tomcat 中 Extension ClassLoader 被集成到了 Bootstrap ClassLoader 里面。
2)System ClassLoader 就是 Application ClassLoader:Tomcat 中的系統(tǒng)類加載器不會加載 CLASSPATH 環(huán)境變量的內(nèi)容,而是從以下資源庫構(gòu)建 System 類加載器。
- $CATALINA_HOME/bin/bootstrap.jar,包含用于初始化Tomcat服務(wù)器的 main() 方法,以及它所依賴的類加載器實現(xiàn)類。
- $CATALINA_BASE/bin/tomcat-juli.jar 或 $CATALINA_HOME/bin/tomcat-juli.jar,日志實現(xiàn)類。
- 如果 $CATALINA_BASE/bin 中存在 tomcat-juli.jar,則使用它來代替 $CATALINA_HOME/bin中的那個。
- $CATALINA_HOME/bin/commons-daemon.jar
3)Common ClassLoader:從名字也看出來來了,主要包含一些通用的類,這些類對 Tomcat 內(nèi)部類和所有 Web 應(yīng)用程序都可見。
該類加載器搜索的位置由 $CATALINA_BASE/conf/catalina.properties 中的 common.loader 屬性定義,默認(rèn)設(shè)置將按照順序搜索以下位置。
- $CATALINA_BASE/lib 中未打包的類和資源
- $CATALINA_BASE/lib 目錄下的JAR 文件
- $CATALINA_HOME/lib 中未打包的類和資源
- $CATALINA_HOME/lib 目錄下的JAR文件
4)WebappX ClassLoader:Tomcat 為每個部署的 Web 應(yīng)用程序創(chuàng)建一個單獨的類加載器,這樣保證了不同應(yīng)用之間是隔離的,類和資源對其他 Web 應(yīng)用是不可見的。加載的路徑如下:
- Web應(yīng)用的 /WEB-INF/classes 目錄下的所有未打包的類和資源
- Web應(yīng)用的 /WEB-INF/lib 目錄下的 JAR 文件中的類和資源
9、Tomcat 的類加載過程?
Tomcat 的類加載過程,也就是 WebappClassLoaderBase#loadClass 的邏輯如下。
1)首先本地緩存 resourceEntries,如果已經(jīng)被加載過則直接返回緩存中的數(shù)據(jù)。
2)檢查 JVM 是否已經(jīng)加載過該類,如果是則直接返回。
3) 檢查要加載的類是否是 Java SE 的類,如果是則使用 BootStrap 類加載器加載該類,以防止 webapp 的類覆蓋了 Java SE 的類。
例如你寫了一個 java.lang.String 類,放在當(dāng)前應(yīng)用的 /WEB-INF/classes 中,如果沒有此步驟的保證,那么之后項目中使用的 String 類都是你自己定義的,而不是 rt.jar 下面的,可能會導(dǎo)致很多隱患。
4)針對委托屬性 delegate 顯示設(shè)置為 true、或者一些特殊的類(javax、org 包下的部分類),使用雙親委派模式加載,只有很少部分使用雙親委派模型來加載。
5)嘗試從本地加載類,如果步驟5中加載失敗也會走到本步驟,這邊打破了雙親委派模型,優(yōu)先從本地進(jìn)行加載。
7)走到這,代表步驟6加載失敗,如果之前不是使用雙親委派模式,則在這邊會委托給父類加載器來嘗試加載。
8)走到這邊代表所有的嘗試都加載失敗,拋出 ClassNotFoundException。
10、JDBC 使用線程上下文類加載器的原理
JDBC 功能相關(guān)的基礎(chǔ)類是由 Java 統(tǒng)一定義的,在 rt.jar 里面,例如 DriverManager,也就是由 Bootstrap ClassLoader 來加載,而 JDBC 的實現(xiàn)類是在各廠商的實現(xiàn) jar 包里,例如 MySQL 是在 mysql-connector-java 里,oracle、sqlserver 也會有各自的實現(xiàn) jar。
此時需要 JDBC 的基礎(chǔ)類調(diào)用其他廠商實現(xiàn)并部署在應(yīng)用程序的 ClassPath 下的 JDBC 服務(wù)提供接口(SPI,Service Provider Interface)的代碼。當(dāng)類A調(diào)用類B時,此時類B是由類A的類加載器來負(fù)責(zé)加載,而 JDBC 的基礎(chǔ)類都是由 Bootstrap ClassLoader 來加載,但是 Bootstrap ClassLoader 是不認(rèn)識也不會去加載這些廠商實現(xiàn)的代碼的。
因此,Java 提供了線程上下文類加載器,允許通過 Thread#setContextClassLoader/Thread#getContextClassLoader() 來設(shè)置和獲取當(dāng)前線程的上下文類加載器。如果創(chuàng)建線程時沒有設(shè)置,則會繼承父線程的,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個類加載器默認(rèn)就是應(yīng)用程序類加載器(Application ClassLoader)。
綜上,JDBC 可以通過線程上下文類加載器,來實現(xiàn)父類加載器“委托”子類加載器完成類加載的行為,這個就明顯不遵守雙親委派模型了,不過這也是雙親委派模型自身的缺陷導(dǎo)致的。
總結(jié)
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Springboot訪問templates html頁面過程詳解
這篇文章主要介紹了Springboot訪問templates html頁面過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-05-05Java8 使用 stream().sorted()對List集合進(jìn)行排序的操作
這篇文章主要介紹了Java8 使用 stream().sorted()對List集合進(jìn)行排序的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10SpringBoot+hutool實現(xiàn)圖片驗證碼
本文主要介紹了SpringBoot+hutool實現(xiàn)圖片驗證碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Spring學(xué)習(xí)之動態(tài)代理(JDK動態(tài)代理和CGLIB動態(tài)代理)
本篇文章主要介紹了Spring學(xué)習(xí)之動態(tài)代理(JDK動態(tài)代理和CGLIB動態(tài)代理),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Java后臺通過Collections獲取list集合中最大數(shù),最小數(shù)代碼
這篇文章主要介紹了Java后臺通過Collections獲取list集合中最大數(shù),最小數(shù)代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08