在上篇文章中棒呛,我們介紹了運行時數(shù)據(jù)區(qū)域、垃圾回收算法域携、Java引用分類等一些有關(guān)于JVM的知識簇秒,今天我們一起來學(xué)習(xí)一下JVM類加載機(jī)制相關(guān)的部分。
定義:虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存秀鞭,并對數(shù)據(jù)進(jìn)行校驗趋观、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型锋边,這就是虛擬機(jī)的類加載機(jī)制皱坛。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止宠默,它的整個生命周期可分為7個階段麸恍,分別為:加載、驗證搀矫、準(zhǔn)備抹沪、解析、初始化瓤球、使用融欧、卸載。如下圖所示:
對于加載的時機(jī)卦羡,JVM沒有做強(qiáng)制約束噪馏,但是對于初始化階段,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有以下5種情況必須立即對類進(jìn)行“初始化”操作:
(1)遇到new绿饵、getstatic欠肾、putstatic或invokestatic這四條字節(jié)碼指令時,如果類沒有進(jìn)行初始化拟赊,則需要先觸發(fā)其初始化刺桃。常見的場景有:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾吸祟、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)瑟慈,以及調(diào)用一個類的靜態(tài)方法的時候桃移。
(2)使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化葛碧,則需要先觸發(fā)其初始化借杰。
(3)當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化进泼,則需要先觸發(fā)其父類的初始化蔗衡。
(4)當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)缘琅,虛擬機(jī)會先初始化這個主類粘都。
(5)當(dāng)使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic刷袍、REF_putStatic、REF_invokeStatic的方法句柄樊展,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化呻纹,則需要先觸發(fā)其初始化。
上述5種場景中的行為专缠,會觸發(fā)類的初始化雷酪,稱之為主動引用。除此之外涝婉,還有一些引用方式不會導(dǎo)致類的初始化哥力,稱為被動引用。常見的被動引用操作有:
(1)通過子類引用父類的靜態(tài)字段墩弯,不會導(dǎo)致子類初始化吩跋。
(2)通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化渔工。
(3)常量在編譯階段會存入調(diào)用類的常量池中锌钮,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化引矩。
1. 加載
在加載階段梁丘,虛擬機(jī)主要完成以下3件事情:
(1)通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
(2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)旺韭。
(3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象氛谜,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
2.驗證
驗證階段的目的:確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求区端,并且不會危害虛擬機(jī)自身的安全值漫。
驗證階段大體可分為四個方面,分別為:文件格式驗證珊燎、元數(shù)據(jù)驗證惭嚣、字節(jié)碼驗證遵湖、符號引用驗證。
3.準(zhǔn)備
準(zhǔn)備階段的作用:為類變量(被static修飾的變量)分配內(nèi)存并且設(shè)置類變量的初始值(數(shù)據(jù)類型零值)晚吞。
4.解析
解析階段的作用:虛擬機(jī)將常量池中的符號引用替換為直接引用延旧。包括有:類或接口的解析、字段解析槽地、類方法解析迁沫、接口方法解析等。
5.初始化
初始化階段:真正開始執(zhí)行類中定義的Java程序代碼捌蚊,初始化類變量和其他資源集畅。或者可以從另一個角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程缅糟。
注:<clinit>()方法:由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生挺智。
類加載器(ClassLoader)
關(guān)于類加載器的知識,想必大家已經(jīng)很熟悉了窗宦,在這里我還是要再提一下赦颇。類加載器作用于類生命周期的加載階段,主要作用可以表述為:將.class文件(字節(jié)碼文件)加載到JVM中赴涵,并轉(zhuǎn)換成相應(yīng)的Class對象媒怯,供JVM識別和使用。
在講解類加載器之前髓窜,我們先明確一點:對于任意一個類扇苞,都需要由加載它的類加載器和這個類本身一同確定其在Java虛擬機(jī)中的唯一性。通俗來講就是:比較兩個類是否“相等”寄纵,只有在這兩個類是由同一加載器加載的前提下才有意義鳖敷,否則,即使這兩個類來源于同一個Class文件擂啥,被同一個虛擬機(jī)加載哄陶,只要加載它們的類加載器不同,那這兩個類就必定不相等哺壶。
Java語言為我們提供了三種類加載器屋吨,分別為:Bootstrap ClassLoader(啟動類加載器)、Extension ClassLoader(擴(kuò)展類加載器)和Application ClassLoader(應(yīng)用程序類加載器)山宾。
Bootstrap ClassLoader(啟動類加載器):使用C++語言實現(xiàn)至扰,是虛擬機(jī)自身的一部分。負(fù)責(zé)加載<JAVA_HOME>\lib目錄下或者被-Xbootclasspath參數(shù)指定路徑下的rt.jar资锰、resources.jar敢课、charsets.jar、.class等。
Extension ClassLoader(擴(kuò)展類加載器):由Java語言實現(xiàn)直秆,獨立于虛擬機(jī)外部濒募。負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄下或者被java.ext.dirs系統(tǒng)變量所指定路徑下的所有類庫。
Application ClassLoader(應(yīng)用程序類加載器圾结、系統(tǒng)類加載器):由Java語言實現(xiàn)瑰剃,獨立于虛擬機(jī)外部。負(fù)責(zé)加載當(dāng)前應(yīng)用的ClassPath目錄下的所有類筝野。
簡單來講晌姚,上述三種類加載器最大的不同之處就是它們所負(fù)責(zé)加載的目錄不同。如果有需要的話歇竟,我們還可以自定義類加載器挥唠,比如從網(wǎng)絡(luò)獲取.class文件并加載到JVM中。
談到類加載器焕议,自然離不開“雙親委派機(jī)制”這個老生常談的話題宝磨,下面我們一起來看一下。
雙親委派機(jī)制要求除了頂層的啟動類加載器之外盅安,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器懊烤。在這里我們要明確一點,上述類加載器之間的關(guān)系并非繼承關(guān)系宽堆。
雙親委派機(jī)制的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類茸习,而是把這個請求委派給父類加載器去完成畜隶,每一個層次的類加載器都是如此,因此所有的加載請求最終傳送到頂層的啟動類加載器号胚,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時籽慢,子加載器才會自己嘗試去加載。
使用雙親委派機(jī)制的好處:保證了Java核心類庫的安全猫胁。它使得Java 類隨著它的類加載器一起具備了一種帶優(yōu)先級的層次關(guān)系箱亿。例如 java.lang.Object 類,無論哪個類加載器去加載該類弃秆,最終都由啟動類加載器加載届惋,因此Object 類在程序的各種類加載器環(huán)境中都是同一個類。相反菠赚,如果沒有使用雙親委派機(jī)制脑豹,由各個類加載器去自行加載的話,用戶自定義一個java.lang.Object 類放在Classpath 目錄下衡查,那么系統(tǒng)中會出現(xiàn)多個不同的Object類瘩欺,導(dǎo)致程序一片混亂。
上面噼里啪啦說了這么多,下面我們一起來驗證一下俱饿,這樣更有助于我們理解和記憶歌粥。
在這里,我們創(chuàng)建一個Person類拍埠,然后在main方法中打印出Person類對應(yīng)的ClassLoader失驶。如圖所示:
運行結(jié)果如下所示:
在這里我們可以看出,平常我們在程序中定義的類是由AppClassLoader加載的械拍。根據(jù)上文中的雙親委托機(jī)制突勇,我們了解到AppClassloader的父加載器為ExtClassLoader,那AppClassloader和ExtClassLoader在代碼中具體有什么關(guān)聯(lián)呢坷虑,我們通過ClassLoader的getParent()方法來看一下:
可以看到甲馋,我們在AppClassloader中調(diào)用了getParent方法,得到了ExtClassLoader對象迄损。我相信你肯定會對它的源碼感興趣定躏。
由上述源碼我們可以知道,AppClassloader和ExtClassLoader都是定義在Launcher中的靜態(tài)內(nèi)部類芹敌,并且兩者都繼承自URLClassLoader痊远。而URLClassLoader最終又是繼承自ClassLoader類。這也驗證了上文中我們所說的雙親委托機(jī)制并非繼承關(guān)系氏捞,而是一種組合關(guān)系碧聪。
Launcher是什么鬼東西呢?它其實是一個java虛擬機(jī)的入口應(yīng)用液茎,我們要想搞清楚AppClassloader的父加載器為什么是ExtClassLoader逞姿,還需要從Launcher的構(gòu)造函數(shù)查找出答案。
在這里我只截取了一部分代碼捆等,大家需要注意的地方就是我打斷點的那兩行代碼滞造。先大致說一下,第一行代碼的作用就是創(chuàng)建了ExtClassLoader對象栋烤,第二行代碼的作用就是創(chuàng)建AppClassloader對象谒养,并在構(gòu)造方法中將之前生成的ExtClassLoader對象作為參數(shù)傳進(jìn)去。在這里我們可以大膽猜想一下明郭,在AppClassLoader中肯定定義了一個parent變量买窟,用來接收getAppClassLoader方法中傳入的參數(shù)。到底是不是這樣呢达址?我們拭目以待蔑祟。
我們跟進(jìn)去getAppClassLoader方法去看一下,結(jié)果如下:
可以看到沉唠,getAppClassLoader方法中最終又是調(diào)用到AppClassLoader中帶有兩個參數(shù)的構(gòu)造函數(shù)疆虚,分別將加載路徑和我們傳過來的classloader(也就是ExtClassLoader對象)傳入,我們接著跟進(jìn)去圖中 2 標(biāo)注的super方法,最終會調(diào)用到ClassLoader類中帶有兩個參數(shù)的構(gòu)造方法径簿,其中var2參數(shù)也就是我們傳過來的ExtClassLoader對象罢屈。
注意看最后一行我用紅筆標(biāo)注的地方,到這里真相大白了吧篇亭。在我們創(chuàng)建AppClassloader對象的時候缠捌,會將之前創(chuàng)建的ExtClassLoader對象作為參數(shù)傳入,在AppClassloader類中定義了一個parent變量译蒂,用來接收我們傳入的ExtClassLoader對象曼月。
那么我們接下來再看一下,ExtClassLoader和BootsTrapClassLoader在代碼中有什么關(guān)聯(lián)呢柔昼?
由運行結(jié)果我們可以看到哑芹,程序拋出了空指針異常。我們判斷肯定是13行調(diào)用了toString方法導(dǎo)致的捕透,這也就說明ExtClassLoader的getParent方法為null聪姿。
what?乙嘀?末购?are you kidding me?虎谢?盟榴?
要想確認(rèn)我們的推斷,我默默的翻起了源碼婴噩。
剛才我們只是分析了斷點第二行曹货,也就是AppClassLoader的創(chuàng)建過程,接下來我們照例分析下斷點第一行ExtClassLoader的創(chuàng)建過程讳推。在這里我們要注意下,調(diào)用getExtClassLoader方法并沒有傳參數(shù)玩般。我們跟進(jìn)去看下:
我們可以看出getExtClassLoader方法最終又是調(diào)用到ExtClassLoader一個參數(shù)的構(gòu)造方法银觅,將加載路徑傳入,我們接下來看下ExtClassLoader一個參數(shù)的構(gòu)造方法坏为。
睜大眼睛注意了究驴,方法中調(diào)用super類時,傳入的第二個參數(shù)匀伏,也就是ClassLoader對象為null洒忧。接下來的調(diào)用就和AppClassLoader類似了,最終會調(diào)用到ClassLoader類中帶有兩個參數(shù)的構(gòu)造方法够颠,其中var2參數(shù)也就是我們傳過來的null熙侍。
這下我們確信了,ExtClassLoader中的parent參數(shù)確定為null。
接下來我們來看下雙親委托機(jī)制的代碼實現(xiàn)(代碼有所刪減):
在我們當(dāng)前ClassLoader進(jìn)行類加載的時候會調(diào)用到loadClass方法蛉抓。方法中首先判斷所要加載的類是否已經(jīng)被加載過庆尘,如果已經(jīng)被加載過了,則直接返回我們加載過的類對象巷送。如果沒有加載過驶忌,則判斷當(dāng)前ClassLoader的parent參數(shù)是否為null,關(guān)于parent參數(shù)的賦值笑跛,我們在上文中已經(jīng)通過源碼分析過了付魔,簡單講,AppClassloader的parent參數(shù)為ExtClassLoader對象飞蹂,而ExtClassLoader的parent參數(shù)為null几苍。例如我們當(dāng)前的ClassLoader為AppClassloader,則parent參數(shù)不為null晤柄,會調(diào)用ExtClassLoader的loadClass方法進(jìn)行類加載擦剑,即AppClassloader的父加載器為ExtClassLoader。例如我們當(dāng)前的ClassLoader為ExtClassLoader芥颈,則parent參數(shù)為null惠勒,會調(diào)用BootstrapClassLoader進(jìn)行類加載,即ExtClassLoader的父加載器為BootstrapClassloader爬坑。在父加載器無法完成類加載的時候纠屋,最終會調(diào)用到當(dāng)前ClassLoader的findClass方法進(jìn)行類加載。
關(guān)于JVM中類加載機(jī)制相關(guān)的知識我們先講到這里盾计,希望在我學(xué)習(xí)的同時也有幫助到大家售担。如果有哪些分析不對的地方,還望老哥們指出署辉,我后續(xù)改正過來族铆。后續(xù)我會接著分析下Android中類加載機(jī)制相關(guān)部分,歡迎大家關(guān)注哭尝。