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