深入了解Java中的類加載機(jī)制
一、類加載過程
程序員編寫的Java源程序(.java文件)在經(jīng)過編譯器編譯之后被轉(zhuǎn)換成字節(jié)代碼(.class 文件),類加載器將.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class對(duì)象,用來(lái)封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。
類加載的最終產(chǎn)品是位于堆區(qū)中的Class對(duì)象,Class對(duì)象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
以下是舉例說明類加載過程:
二、類生命周期
類的生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載7個(gè)階段。其中加載、驗(yàn)證、準(zhǔn)備、初始化、卸載5個(gè)階段是按照這種順序按部就班的開始,而解析階段則不一定:某些情況下,可以在初始化之后再開始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定,其實(shí)就是多態(tài)),例如子類重寫父類方法。
注意:這里寫的是按部就班的開始,而不是按部就班地進(jìn)行或完成,因?yàn)檫@些階段通常都是互相交叉混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行過程中調(diào)用、激活另外一個(gè)階段。
1、加載
加載階段會(huì)做3件事情:
- 通過一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問入口。
此處第一點(diǎn)并沒指明要從哪里獲取、怎樣獲取,因此這里給開發(fā)人員預(yù)留了擴(kuò)展空間。許多Java技術(shù)就建立在此基礎(chǔ)上,例如:
- 從ZIP包讀取,如JAR、WAR。
- 從網(wǎng)絡(luò)中獲取,這種場(chǎng)景最典型應(yīng)用場(chǎng)景應(yīng)用就是Applet。
- 運(yùn)行時(shí)計(jì)算生成,使用較多場(chǎng)景是動(dòng)態(tài)代理技術(shù),如spring AOP。
加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,而且在Java堆中也創(chuàng)建一個(gè)java.lang.Class類的對(duì)象,這樣便可以通過該對(duì)象訪問方法區(qū)中的這些數(shù)據(jù)。
2、驗(yàn)證
確保被加載的類的正確性,分為4個(gè)驗(yàn)證階段:
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證
- 字節(jié)碼驗(yàn)證
- 符號(hào)引用驗(yàn)證
驗(yàn)證階段非常重要的,但不是必須的,它對(duì)程序運(yùn)行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。
3、準(zhǔn)備
為類的靜態(tài)變量分配內(nèi)存,并初始化默認(rèn)值,這些內(nèi)存是在方法區(qū)中分配,需要注意以下幾點(diǎn):
- 此處內(nèi)存分配的變量?jī)H包含類變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)隨著對(duì)象實(shí)例化被分配在java堆中。
- 這里默認(rèn)值是數(shù)據(jù)類型的默認(rèn)值(如0、0L、null、false),而不是代碼中被顯示的賦予的值。
- 如果類字段的字段屬性表中存在ConstatntValue屬性,即同時(shí)被final和static修飾,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstValue屬性所指定的值。
4、解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程,解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量。
直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。
5、初始化
為類的靜態(tài)變量賦予正確的初始值,JVM負(fù)責(zé)對(duì)類進(jìn)行初始化,主要對(duì)類變量進(jìn)行初始化。初始化階段是執(zhí)行類構(gòu)造器<client>()
方法的過程。
<client>()
方法是由編譯器自動(dòng)收集類中的所有類變量賦值動(dòng)作和靜態(tài)語(yǔ)句static{}
塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件出現(xiàn)的順序所決定的。靜態(tài)語(yǔ)句塊中只能訪問到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在之后的變量可以賦值,但不能訪問。如下所示:
public class Test{ static{ i=0; System.out.print(i); } static int i=1; }
<clinit>()
方法與類構(gòu)造函數(shù)不一樣,不需要顯示調(diào)用父類構(gòu)造函數(shù),虛擬機(jī)會(huì)保證在子類的<clinit>()
方法執(zhí)行之前,父類的<clinit>()
方法已執(zhí)行完畢。
由于父類的<clinit>()
方法首先執(zhí)行,意味著父類中的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作,如下所示,最終得出的值是2,而不是1。
public class TestClassLoader { public static int A = 1; static { A = 2; // System.out.println(A); } static class Sub extends TestClassLoader { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); } }
<clinit>()
方法對(duì)于類和接口來(lái)說,并不是必須的,若類沒有靜態(tài)語(yǔ)句塊,也沒有對(duì)變量賦值操作,則不會(huì)生成<clinit>()
方法。
接口與類不同的是,接口不需要先執(zhí)行父類的<clinit>()
方法,只有父接口定義的變量使用時(shí),父接口才會(huì)被初始化。另外接口的實(shí)現(xiàn)類也不會(huì)先執(zhí)行接口的<clinit>()
方法。
虛擬機(jī)保證當(dāng)多線程去初始化類時(shí),只會(huì)有一個(gè)線程去執(zhí)行<clinit>()
方法,而其他線程則被阻塞。
<clinit>()
方法和<init>()
方法區(qū)別:
執(zhí)行時(shí)機(jī)不同:init方法是對(duì)象構(gòu)造器方法,在new一個(gè)對(duì)象并調(diào)用該對(duì)象的constructor方法時(shí)才會(huì)執(zhí)行。clinit方法是類構(gòu)造器方法,是在JVM加載期間的初始化階段才會(huì)調(diào)用。
執(zhí)行目的不同:init是對(duì)非靜態(tài)變量解析初始化,而clinit是對(duì)靜態(tài)變量,靜態(tài)代碼塊進(jìn)行初始化。
三、雙親委派機(jī)制
在介紹雙親委派機(jī)制前,先來(lái)看下類加載器的層次關(guān)系圖,如下:
- 啟動(dòng)類加載器(Bootstrap ClassLoader),負(fù)責(zé)加載存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機(jī)識(shí)別的類庫(kù)(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動(dòng)類加載器是無(wú)法被Java程序直接引用的。
- 擴(kuò)展類加載器(Extension ClassLoader),該加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載$JAVA_HOME\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(kù)(如javax.*開頭的類),開發(fā)者可以直接使用擴(kuò)展類加載器。
- 應(yīng)用程序類加載器(Application ClassLoader),該類加載器由sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
- 自定義類加載器(User ClassLoader),如果有必要,我們還可以加入自定義的類加載器。因?yàn)镴VM自帶的ClassLoader只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的java class文件。
雙親委派機(jī)制是指如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把請(qǐng)求委托給父加載器去完成,依次向上,因此,所有的類加載請(qǐng)求最終都應(yīng)該被傳遞到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時(shí),即無(wú)法完成該加載,子加載器才會(huì)嘗試自己去加載該類。
為了更清楚的了解雙親委派機(jī)制,我們來(lái)看下jdk1.8源碼java.lang.ClassLoader.loadClass()方法實(shí)現(xiàn):
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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; } }
上面代碼注釋寫的很清楚,首先調(diào)用findLoadedClass方法檢查是否已加載過這個(gè)類,如果沒有就調(diào)用parent的loadClass方法,從底層一級(jí)級(jí)往上。如果所有ClassLoader都沒有加載過這個(gè)類,就調(diào)用findClass方法查找這個(gè)類,然后又從頂層逐級(jí)向下調(diào)用findClass方法,最終都沒找到就拋出ClassNotFoundException。這樣設(shè)計(jì)的目的是保證安全性,防止系統(tǒng)類被偽造。
為了便于理解,以下是加載邏輯示意圖:
四、自定義類加載器的應(yīng)用
自定義類加載器通常有以下四種應(yīng)用場(chǎng)景:
- 源代碼加密,防止源碼泄露
- 隔離加載類,采用隔離加載,防止依賴沖突。
- 修改類加載的方式。
- 擴(kuò)展加載源。
1、源代碼加密
源代碼加密的本質(zhì)是對(duì)字節(jié)碼文件進(jìn)行操作。我們可以在打包的時(shí)候?qū)lass進(jìn)行加密操作,然后在加載class文件之前通過自定義classloader先進(jìn)行解密操作,然后再按照標(biāo)準(zhǔn)的class文件標(biāo)準(zhǔn)進(jìn)行加載,這樣就完成了class文件正常的加載。因此這個(gè)加密的jar包只有能夠?qū)崿F(xiàn)解密方法的classloader才能正常加載。
2、隔離加載類
我們常常遇到頭疼的事情就是jar包版本的依賴沖突,寫代碼五分鐘,排包一整天。
舉個(gè)栗子:
工程里面同時(shí)引入了 A、B 兩個(gè) jar 包,以及 C 的 v0.1、v0.2 版本,v2 版本的 Log 類比 v1 版本新增了 error 方法,,打包的時(shí)候 maven 只能選擇 C 的一個(gè)版本,假設(shè)選擇了 v1 版本。到了運(yùn)行的時(shí)候,默認(rèn)情況下一個(gè)項(xiàng)目的所有類都是用同一個(gè)類加載器加載的,所以不管你依賴了多少個(gè)版本的 C,最終只會(huì)有一個(gè)版本的 C 被加載到 JVM 中。當(dāng) B 要去訪問 Log.error,就會(huì)發(fā)現(xiàn) Log 壓根就沒有 error 方法,然后就拋異常 java.lang.NoSuchMethodError。這就是類沖突的一個(gè)典型案例。
類隔離技術(shù)就是用來(lái)解決這個(gè)問題。讓不同模塊的 jar 包用不同的類加載器加載。
JVM 提供了一種非常簡(jiǎn)單有效的方式,我把它稱為類加載傳導(dǎo)規(guī)則:JVM 會(huì)選擇當(dāng)前類的類加載器來(lái)加載所有該類的引用的類。例如我們定義了 TestA 和 TestB 兩個(gè)類,TestA 會(huì)引用 TestB,只要我們使用自定義的類加載器加載 TestA,那么在運(yùn)行時(shí),當(dāng) TestA 調(diào)用到 TestB 的時(shí)候,TestB 也會(huì)被 JVM 使用 TestA 的類加載器加載。依此類推,只要是 TestA 及其引用類關(guān)聯(lián)的所有 jar 包的類都會(huì)被自定義類加載器加載。通過這種方式,我們只要讓模塊的 main 方法類使用不同的類加載器加載,那么每個(gè)模塊的都會(huì)使用 main 方法類的類加載器加載的,這樣就能讓多個(gè)模塊分別使用不同類加載器。這也是 OSGi 和 SofaArk 能夠?qū)崿F(xiàn)類隔離的核心原理。
3、熱加載/熱部署
在應(yīng)用運(yùn)行的時(shí)升級(jí)軟件,無(wú)需重新啟動(dòng)的方式有兩種,熱部署和熱加載。
對(duì)于Java應(yīng)用程序來(lái)說,熱部署就是在服務(wù)器運(yùn)行時(shí)重新部署項(xiàng)目,熱加載即在運(yùn)行時(shí)重新加載class,從而升級(jí)應(yīng)用。
熱加載可以概括為在容器啟動(dòng)的時(shí)候起一條后臺(tái)線程,定時(shí)的檢測(cè)類文件的時(shí)間戳變化,如果類的時(shí)間戳變掉了,則將類重新載入。對(duì)比反射機(jī)制,反射是在運(yùn)行時(shí)獲取類信息,通過動(dòng)態(tài)的調(diào)用來(lái)改變程序行為。而熱加載則是在運(yùn)行時(shí)通過重新加載改變類信息,直接改變程序行為。
熱部署原理類似,但它是直接重新加載整個(gè)應(yīng)用,這種方式會(huì)釋放內(nèi)存,比熱加載更加干凈徹底,但同時(shí)也更費(fèi)時(shí)間。
4、擴(kuò)展加載源
字節(jié)碼文件可以從數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)、移動(dòng)設(shè)備、甚至是電視機(jī)機(jī)頂盒進(jìn)行加載,可以與源代碼加密方式搭配使用。比如部分關(guān)鍵代碼可以通過移動(dòng)U盤讀取再加載到JVM。
到此這篇關(guān)于深入了解Java中的類加載機(jī)制的文章就介紹到這了,更多相關(guān)Java類加載機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)簡(jiǎn)單推箱子游戲
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)推箱子游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06Java kafka如何實(shí)現(xiàn)自定義分區(qū)類和攔截器
這篇文章主要介紹了Java kafka如何實(shí)現(xiàn)自定義分區(qū)類和攔截器,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06Java解除文件占用即Dom4j操作后實(shí)現(xiàn)xml關(guān)流
這篇文章主要介紹了Java解除文件占用即Dom4j操作后實(shí)現(xiàn)xml關(guān)流,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04Java Clone深拷貝與淺拷貝的兩種實(shí)現(xiàn)方法
今天小編就為大家分享一篇關(guān)于Java Clone深拷貝與淺拷貝的兩種實(shí)現(xiàn)方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10Java中String類常用類型實(shí)例總結(jié)
在我們開發(fā)中經(jīng)常會(huì)用到很多的常用的工具類,這里做一個(gè)總結(jié),下面這篇文章主要給大家介紹了關(guān)于Java中String類常用類型的相關(guān)資料,String類代表字符串,需要的朋友可以參考下2021-12-12SpringBoot定時(shí)任務(wù)參數(shù)運(yùn)行代碼實(shí)例解析
這篇文章主要介紹了SpringBoot定時(shí)任務(wù)運(yùn)行代碼實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06