上篇中我們講解了Class文件,這篇我們說(shuō)說(shuō)虛擬機(jī)是如何加載這些Class文件的?Class文件中的信息進(jìn)入到虛擬機(jī)后會(huì)發(fā)生什么變化?這就涉及到了類加載機(jī)制。
類加載機(jī)制是把類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的java類型。這一系列的過(guò)程都是在程序運(yùn)行期間完成的。
類加載器
類加載器就是下圖中紅框的部分,它通過(guò)一個(gè)類的全限定名來(lái)獲取描述此類的二進(jìn)制字節(jié)流,從而將java類動(dòng)態(tài)地加載進(jìn)JVM的內(nèi)存空間中。
適用情景
對(duì)于一個(gè)非數(shù)組類的加載階段,可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成。
對(duì)于數(shù)組類而言,其由java虛擬機(jī)直接創(chuàng)建,不通過(guò)類加載器。
雙親委派機(jī)制
雙親委派機(jī)制是類加載所采取的一種方式。如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成。每一層的類加載器均是如此。只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)請(qǐng)求時(shí),子加載器才會(huì)嘗試自己去加載。
類比到現(xiàn)實(shí):小明想買一個(gè)玩具挖土機(jī),可他又不好意思直接張口說(shuō)。所以,發(fā)生了下面的對(duì)話。
小明去問(wèn)他爸爸:爸爸你有挖土機(jī)嗎?
爸爸說(shuō):沒有哎
接著爸爸問(wèn)爺爺:爸爸爸爸,你有挖土機(jī)嗎?
爺爺說(shuō):沒有哎
接著爺爺問(wèn)太爺爺:爸爸爸爸,你有挖土機(jī)嗎?
太爺爺說(shuō):我也沒有。讓重孫子去買一個(gè)吧。
結(jié)果小明就高高興興地自己去買了一個(gè)玩具挖土機(jī)。
分類
啟動(dòng)類加載器是使用C++實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分。
其它類加載器是由java語(yǔ)言實(shí)現(xiàn)的,獨(dú)立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader。
好處
以String類為例。就算是用戶自己寫了一個(gè)String類的實(shí)現(xiàn),那對(duì)此類進(jìn)行加載時(shí),也只會(huì)委派給啟動(dòng)類加載器來(lái)對(duì)JDK中原本的String類進(jìn)行加載,而自定義的String類永遠(yuǎn)不會(huì)被調(diào)用。這樣保證了系統(tǒng)的安全。
什么時(shí)候進(jìn)行類加載?
有且只有以下5種方式必須立即對(duì)類進(jìn)行加載
(1)使用new實(shí)例化對(duì)象的時(shí)候;讀取或配置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候;調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
(2)使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候。如果類沒有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
(3)當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類的初始化。
(4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的類),虛擬機(jī)會(huì)先初始化這個(gè)主類
類加載過(guò)程詳述
類加載過(guò)程分為5步。大部分都是由虛擬機(jī)主導(dǎo)和控制的,除了以下兩種情形:
在加載階段
開發(fā)人員可以通過(guò)自定義類加載器參與
在初始化階段
會(huì)執(zhí)行開發(fā)人員的代碼去初始化類變量和其它資源
1、加載
虛擬機(jī)需要完成的事情:
(1) 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
(2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
(3)在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。
2、驗(yàn)證
驗(yàn)證的目的是確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,不會(huì)危害虛擬機(jī)自身的安全。
其分為4個(gè)步驟:文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證。其中文件格式驗(yàn)證是直接對(duì)字節(jié)流進(jìn)行操作的,其余3項(xiàng)是在方法區(qū)中進(jìn)行的。
3、準(zhǔn)備
此階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段。其是在方法區(qū)中進(jìn)行分配的。有兩個(gè)注意點(diǎn):
(1)此時(shí)只是對(duì)類變量(被static修飾的變量)進(jìn)行內(nèi)存分配,而不是對(duì)象變量。給對(duì)象分配內(nèi)存是在對(duì)象實(shí)例化時(shí),隨著對(duì)象一起分配到j(luò)ava堆中。
(2)如果一個(gè)類變量沒有被final修飾,則其初始值是數(shù)據(jù)類型的零值。比如int類型的是0,boolean類型的是false。舉個(gè)例子來(lái)說(shuō)明:
public static int value=123;
在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@個(gè)時(shí)候尚未開始執(zhí)行任何java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器< clinit >()方法之中。所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
public static final int value=123;
此時(shí)因?yàn)橛衒inal,所以在準(zhǔn)備階段value就已經(jīng)被賦值為123了。
4、解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程??蓪?duì)類或接口、字段、類方法、接口方法等進(jìn)行解析。
符號(hào)引用是什么:
符號(hào)引用就是包含類的信息,方法名,方法參數(shù)等信息的字符串,它供實(shí)際使用時(shí)在該類的方法表中找到對(duì)應(yīng)的方法。
直接引用是什么:
直接引用就是偏移量,通過(guò)偏移量可以直接在該類的內(nèi)存區(qū)域中找到方法字節(jié)碼的起始位置。
符號(hào)引用是告訴你此方法的一些特征,你需要通過(guò)這些特征去尋找對(duì)應(yīng)的方法。直接引用就是直接告訴你此方法在哪。
5、初始化
此階段用于初始化類變量和其它資源,是執(zhí)行類構(gòu)造器< clinit >()方法的過(guò)程,此時(shí)才是真正開始執(zhí)行類中定義的java程序代碼。
以上是對(duì)JAVA虛擬機(jī)類加載機(jī)制的詳細(xì)講解,