面試必問:JVM的類加載機制
1 類加載過程
想要使用一個類,首先需要將其加載到JVM中,類加載到JVM需要經(jīng)過三個步驟:加載->鏈接->初始化。其中鏈接又分為驗證,準備,解析三步。
1.1 加載
類加載階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法這個類的各個數(shù)據(jù)的入口。注意相關(guān)信息不是一定要從Class文件中獲取,它即可從ZIP中讀取(如從Jar包,War包中讀取),也可以在運算時生成(動態(tài)代理),也可以由其它文件生成(如將JSP文件轉(zhuǎn)換成對應(yīng)的Class類)。
1.2 鏈接
鏈接過程分為驗證,準備,解析三步。
1.2.1 驗證
JAVA是一種相對安全的語言,驗證的意義是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機要求。不會危害到虛擬機的安全。驗證主要包含文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證。
文件格式驗證:驗證字節(jié)流是否符合Class文件格式規(guī)范,并且能被當(dāng)前JVM加載處理。如常量類型是否支持。
元數(shù)據(jù)驗證:字節(jié)碼信息進行語言分析,分析是否符合java語言規(guī)范。
字節(jié)碼驗證:最重要的驗證環(huán)節(jié),元數(shù)據(jù)驗證后對方法體驗證,保證類方法在運行時不會有危害發(fā)生。
符號引用驗證:驗證符號引用,保證能夠訪問到,不會出現(xiàn)無法訪問的情況。
1.2.2 準備
準備階段正式對類變量分配內(nèi)存,并設(shè)置初始默認值。如定義 private static int s = 2020;經(jīng)過此階段s=0,而不是2020.但是如果定義為private final static int s = 2020,在準備階段會直接將s設(shè)置成2020而不是初始值0;
1.2.3 解析
解析階段JVM將常量池中的符號引用替換為直接引用。準備階段只是分配了內(nèi)存,但是類變量并沒有指向那一塊內(nèi)存,這一步就是完成實際指向的工作。
1.3 初始化
初始化階段為類變量設(shè)置正確的初始值。如上文 private static int s = 2020中將s賦值2020的動作便在這一步完成。同時初始化階段也會執(zhí)行靜態(tài)代碼塊。如果有超類,則先對超類執(zhí)行初始化。
初始化階段是執(zhí)行類構(gòu)造器
以下幾種情況不會觸發(fā)初始化:
子類引用父類的靜態(tài)變量,只會觸發(fā)父類的初始化,不會觸發(fā)子類的初始化
定義對象數(shù)組,不會觸發(fā)初始化
常量在編譯過程中直接存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類的初始化。如A引用B類的常量final static x = 3。此時不會對B進行初始化。
通過類名獲取Class對象,不會觸發(fā)類的初始化。
通過Class.forName加載指定類時,如果initalize為false,不會觸發(fā)初始化。
通過ClassLoader默認的loadClss方法,也不會觸發(fā)初始化。
類加載的動作由類加載器完成。對于JVM來說,類的唯一性是通過類的全限定名+類加載器來區(qū)分的。不同的類加載器加載的同一個類并不被認為是同一類。如有一個類C,分別用類加載器CL1,CL2加載。一個參數(shù)需要CL1的C實例,傳入的確是CL2的C實例,就會報錯java.lang.ClassCastException:C cannot be cast to C。
JVM提供了三種類加載器:啟動類加載器(Bootstrap ClassLoader),擴展類加載器(Extension ClassLoader),應(yīng)用程序類加載器(Application ClassLoader).
啟動類加載器:用來加載Java的核心庫,主要加載的是JVM自身所需要的類,使用C++實現(xiàn),并非繼承于java.lang.ClassLoader,是JVM的一部分。負責(zé)加載JAVA_HOME\lib目錄中的,或者-Xbootclasspath參數(shù)指定的路徑中的,且被虛擬機認可[注1]的類。開發(fā)者無法直接獲取到其引用。
注1:JVM是按文件名識別的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包放在lib目錄下也沒有作用,同時啟動加載器只加載包名為java,javax,sun等開頭的類。且java是特殊包名,開發(fā)者的包名不能以java開頭,如果自定義了一個java.***包來讓類加載器加載,那么就會拋出異常java.lang.SecurityException: Prohibited package name: java.***擴展類加載器:用來加載Java的擴展庫。負責(zé)加載JAVA_HOME\lib\ext目錄中的,或通過系統(tǒng)變量java.ext.dirs指定路徑中的類庫。由java語言實現(xiàn)。開發(fā)者可以直接使用。
應(yīng)用程序類加載器:負責(zé)加載用戶路徑(classpath)上的類庫。開發(fā)者可以直接使用。可以通過ClassLoader.getSystemClassLoader()獲得。一般情況下程序的默認類加載器就是該加載器。
除了提供的加載器外,開發(fā)者可以通過繼承ClassLoader類的方式實現(xiàn)自己的類加載器。
JVM的類加載機制為雙親委派機制,除了頂層的啟動類加載器,雙親委派機制要求每一個類加載器都要有自己的父加載器。這個父加載器并不是指繼承,而是一種委派關(guān)系。雙親委派機制下一個類加載器收到類加載請求不會直接自己去加載,而是先把這個請求委托給自己的父類加載器去執(zhí)行,如果父類加載器還存在父類加載器,則繼續(xù)委托,一直委托到最頂層的啟動類加載器。父加載器可以加載目標類的話就由父加載器完成加載任務(wù),如果父加載器無法完成則子加載器自己嘗試加載。這就是雙親委派機制。
雙親委派機制的優(yōu)勢:雙親委派機制使得java類隨著他的類加載器具備了一種帶有帶有優(yōu)先級的層級關(guān)系。通過這種層級關(guān)系可以避免類的的重復(fù)加載,當(dāng)父加載器已經(jīng)加載過目標類時,子加載器無需重復(fù)加載一次。
其次是安全方面,通過雙親委派機制可以避免核心類不會被隨意替換,例如網(wǎng)絡(luò)傳遞一個名為java.lang.Integer的類,在雙親委派機制下,加載請求會被傳遞到頂層的啟動類加載器,啟動類加載器發(fā)現(xiàn)這個名字的類已經(jīng)加載過了,就會直接返回已經(jīng)加載過的Integer.class。這樣就可以防止核心API被修改。
OSGI(Open Service Gateway Initiative)是面向java的動態(tài)模型系統(tǒng)。OSGI能夠提供無需重啟的動態(tài)改造功能,基于OSGI的程序很可能可以實現(xiàn)模塊級的熱插拔功能,當(dāng)程序進行升級時,只需停用,重新安裝然后啟動程序的一部分。但并非所有的程序都適合OSGI架構(gòu),其在提供強大功能的同時也提高了復(fù)雜度,因為OSGI不支持雙親委派機制。
eclipse就是基于OSGI技術(shù)來構(gòu)建的。
OSGI每個模塊都自己的類,每個模塊可以聲明其需要模塊的類(導(dǎo)入),也可以聲明自己的類以供其它模塊使用(導(dǎo)出),每個模塊都有自己的類加載器,他負責(zé)加載本模塊的類,對于非本模塊的類:核心庫的類會代理給父類加載器(通常是啟動類加載器),其他模塊導(dǎo)入的類則代理給對應(yīng)模塊由其加載。對于java開頭的類,默認都是由父類加載器完成的,可以通過org.osgi.framework.bootdelegation設(shè)置某些包或者類必須由父類加載器加載。如設(shè)置org.osgi.framework.bootdelegation = com.my.* 此時com.my下的所有類都由父類加載器進行加載
例如由兩個模塊:M-A和M-B,分別有類C-A和C-B,C-A繼承自C-B,M-A啟動時,對C-A進行加載,因為繼承關(guān)系繼而需要加載C-B,此時由于M-A聲明了C-B是由M-B導(dǎo)入的,那么就會將C-B的加載交由M-B執(zhí)行,M-B對C-B進行加載,所得的類實例可以被所有聲明導(dǎo)入此類的模塊使用。
本文鏈接:https://blog.csdn.net/yue_hu/article/details/109622483
end
*版權(quán)聲明:轉(zhuǎn)載文章和圖片均來自公開網(wǎng)絡(luò),版權(quán)歸作者本人所有,推送文章除非無法確認,我們都會注明作者和來源。如果出處有誤或侵犯到原作者權(quán)益,請與我們聯(lián)系刪除或授權(quán)事宜。
長按識別圖中二維碼
關(guān)注獲取更多資訊
不點關(guān)注,我們哪來故事?

點個再看,你最好看
