JVM实战与原理---类加载机制
JVM實(shí)戰(zhàn)與原理
目錄
類(lèi)加載機(jī)制
1. 類(lèi)加載生命周期
1.1 加載
1.2 驗(yàn)證
1.3 準(zhǔn)備
1.4 解析
1.5 初始化
2. 類(lèi)加載器
類(lèi)加載機(jī)制
章節(jié)目的:了解虛擬機(jī)如何加載Class文件?Class文件的信息進(jìn)入到虛擬機(jī)后會(huì)發(fā)生什么變化
引言:在了解了Class文件結(jié)構(gòu)后,我們就會(huì)想知道Class文件是怎么被虛擬機(jī)加載的
下面便是類(lèi)加載機(jī)制的介紹。
1. 類(lèi)加載生命周期
類(lèi)的加載生命周期分為七步,加載->驗(yàn)證->準(zhǔn)備->解析->初始化->使用->卸載
1.1 加載
加載階段,虛擬機(jī)需完成以下三件事:
1. 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流,比如java.lang.Object就是一個(gè)類(lèi)的全限定名
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è)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口
1.2 驗(yàn)證
驗(yàn)證階段是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)本身。驗(yàn)證階段,虛擬機(jī)會(huì)完成下面4個(gè)校驗(yàn)動(dòng)作
1. 文件格式驗(yàn)證:該階段主要為了保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),大致包含:
是否以CAFEBABE開(kāi)頭、版本號(hào)是否在JVM處理范圍內(nèi)、常量池tag標(biāo)志是否支持、索引指向異常、編碼不符合規(guī)范等
2. 元數(shù)據(jù)驗(yàn)證:該階段主要為了對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),大致包含:
是否有父類(lèi)、是否繼承了不允許繼承的類(lèi)、非抽象類(lèi)是否實(shí)現(xiàn)所有方法、是否與父類(lèi)沖突等
3. 字節(jié)碼驗(yàn)證:該階段主要為了對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證類(lèi)方法在運(yùn)行時(shí)不會(huì)做出傷害虛擬機(jī)安全的事件,大致包含:
操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列能配合工作、跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上、類(lèi)型轉(zhuǎn)換是有效的等
4. 符號(hào)引用驗(yàn)證:該階段主要為了確保解析動(dòng)作能正常執(zhí)行,大致包含:
符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類(lèi)、符號(hào)引用中的類(lèi)、字段、方法的訪問(wèn)性是否可被當(dāng)前類(lèi)訪問(wèn)等
1.3 準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。有兩點(diǎn)需強(qiáng)調(diào)
1. 此時(shí)內(nèi)存分配的僅包括被static修飾的變量,實(shí)例變需等對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在堆中
2. 初始值是數(shù)據(jù)類(lèi)型的零值,比如static int value = 123;初始值是0而不是123。如果字段屬性表中存在ConstantValue屬性,那么準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所設(shè)置的值,如static final int value = 123;則初始值就是123
1.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)行。
分別對(duì)應(yīng)CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MedhodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info。我們抽幾個(gè)來(lái)講
1.4.1?類(lèi)或接口的解析
假設(shè)有一個(gè)D類(lèi),需將符號(hào)引用N解析為一個(gè)類(lèi)或接口C的直接引用,需完成以下3步
1 如果C不是數(shù)組,虛擬機(jī)會(huì)把代表N的全限定名傳遞給D的類(lèi)加載器去加載類(lèi)C
2 如果C是數(shù)組,并且數(shù)組的元素類(lèi)型為對(duì)象,N的描述符會(huì)是類(lèi)似Ljava/lang/Integer形式,會(huì)按照1.1加載數(shù)組元素類(lèi)型(java.lang.Integer),接著虛擬機(jī)生成一個(gè)代表次數(shù)組維度和元素的數(shù)組對(duì)象
3 經(jīng)過(guò)上面兩步,C在虛擬機(jī)已經(jīng)此成為一個(gè)有效的類(lèi)或接口,接著進(jìn)行符號(hào)引用驗(yàn)證,確認(rèn)D是否具備對(duì)C的訪問(wèn)權(quán)限,不具備權(quán)限則拋出java.lang.IllegalAccessError異常。
1.4.2?字段解析
要解析字段符號(hào)引用,需現(xiàn)對(duì)字段表內(nèi)class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用解析,如解析異常,則字段符號(hào)引用解析也會(huì)失敗。字段解析分四步
假設(shè)字段所屬的類(lèi)或接口用C表示
1 如果C本身就包含簡(jiǎn)單名稱和字段描述符都與目標(biāo)匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束
2 否則,如果在C中實(shí)現(xiàn)了接口,會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口,如果接口中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束
3 否則,如果C不是java.lang.Object的話,講會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類(lèi),如果在父類(lèi)中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束
4 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。如果查找成功返回了引用,將會(huì)對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證,如不具備訪問(wèn)權(quán)限,則拋出java.lang.IllegalAccessError異常。
1.4.3?類(lèi)方法解析
類(lèi)方法解析的第一個(gè)步驟與字段解析一樣,需要先解析出類(lèi)方法表的class_index項(xiàng)中索引的方法所屬的類(lèi)或接口的符號(hào)引用。類(lèi)方法解析分五步
假設(shè)C表示這個(gè)類(lèi)
1 如果在類(lèi)方法表中發(fā)表class_index中索引的C是個(gè)接口,就直接拋出java.lang.IncompatibleClassChangeError異常
2 在類(lèi)C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
3 否則,在類(lèi)C的父類(lèi)中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
4 否則,在類(lèi)C實(shí)現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果存在匹配的方法,說(shuō)明C是一個(gè)抽象類(lèi),這時(shí)查找結(jié)束,拋出java.lang.AbstractMethodError異常。
5 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。
如果查找過(guò)程返回了直接引用,會(huì)對(duì)方法進(jìn)行權(quán)限驗(yàn)證,如不具備訪問(wèn)權(quán)限,則拋出java.lang.IllegalAccessError異常。
1.4.4?接口方法解析
接口方法也需要先解析接口方法表的class_index項(xiàng)中索引的方法所屬的類(lèi)或接口的符號(hào)引用。接口方法解析分四步
假設(shè)C表示這個(gè)類(lèi)
1?如果在類(lèi)方法表中發(fā)表class_index中索引的C是個(gè)類(lèi)而不是接口,就直接拋出java.lang.IncompatibleClassChangeError異常
2 在接口C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
3 否則,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類(lèi)為止,看是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
4 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。
因?yàn)榻涌诜椒J(rèn)都是public的,所以不存在訪問(wèn)權(quán)限的問(wèn)題。
1.5 初始化
初始化階段會(huì)根據(jù)程序去初始化變量和其他資源,即執(zhí)行類(lèi)構(gòu)造器<clinit>方法的過(guò)程,<clinit>方法有以下特點(diǎn)
1. <clinit>方法是由編譯器收集類(lèi)中所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的
2. <clinit>方法與類(lèi)的狗喊函數(shù)<init>方法不同,不需要顯式調(diào)用父類(lèi)構(gòu)造器,虛擬機(jī)會(huì)保證在子類(lèi)<clinit>執(zhí)行前,父類(lèi)<clinit>已執(zhí)行完成
3. 由于父類(lèi)<clinit>先執(zhí)行,故父類(lèi)定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類(lèi)
4. 如果一個(gè)類(lèi)沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不生車(chē)給<clinit>方法
5. 接口沒(méi)有靜態(tài)語(yǔ)句塊,但又類(lèi)變量賦值,因此接口也會(huì)生成<clinit>方法。接口<clinit>方法不需要先執(zhí)行父接口的<clinit>方法,接口實(shí)現(xiàn)類(lèi)初始化時(shí)也不會(huì)執(zhí)行接口的<clinit>方法
6. 虛擬機(jī)會(huì)保證<clinit>方法在多線程環(huán)境被正確地枷鎖、同步,如果多個(gè)線程同時(shí)初始化一個(gè)類(lèi),指揮有一個(gè)線程執(zhí)行類(lèi)的<clinit>方法,其他線程都阻塞等待,知道<clinit>方法執(zhí)行完畢。如果類(lèi)的<clinit>方法耗時(shí)長(zhǎng),則可能導(dǎo)致多個(gè)進(jìn)程阻塞。
2. 類(lèi)加載器
作用:實(shí)現(xiàn)類(lèi)的加載動(dòng)作,同時(shí)類(lèi)本身與加載它的類(lèi)加載器一同確定在Java虛擬機(jī)中的唯一性。
比如使用equals方法,instanceof關(guān)鍵字時(shí),必須保證同一個(gè)Class文件,被同一個(gè)虛擬機(jī),用同樣的類(lèi)加載器加載,兩個(gè)類(lèi)才是相等的。
雙親委派模型
如果我們自己新寫(xiě)一個(gè)java.lang.Object類(lèi)放入ClassPath中,此時(shí),虛擬機(jī)會(huì)使用rt.jar包里的Object作為運(yùn)行類(lèi)還是我們新寫(xiě)的Object作為運(yùn)行類(lèi)呢?
答案是虛擬機(jī)還是使用rt.jar包中的Object類(lèi),而我們新寫(xiě)的Object類(lèi)永遠(yuǎn)無(wú)法被加載運(yùn)行,這樣保證了Java程序的穩(wěn)定運(yùn)行。
那么為什么虛擬機(jī)會(huì)去加載rt.jar包里的Object類(lèi)呢?答案就是雙親委派模型。
雙親委派模型是什么:
雙親委派模型將類(lèi)加載器分為四層,除了頂層,其余類(lèi)加載器都有自己的父類(lèi)加載器。頂層類(lèi)加載器由C++實(shí)現(xiàn),其余由Java實(shí)現(xiàn)。
啟動(dòng)類(lèi)加載器:頂層類(lèi)加載器,負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄或被-Xbootclasspath參數(shù)所指定路徑下,固定文件名(如rt.jar)的類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中。
擴(kuò)展類(lèi)加載器:第二層類(lèi)加載器,負(fù)責(zé)將存放在<JAVA_HOME>\lib\ext目錄或被java.ext.dirs系統(tǒng)變量所指定的路徑下的類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中
應(yīng)用程序類(lèi)加載器:第三層類(lèi)加載器,負(fù)責(zé)將存放在用戶類(lèi)路徑(ClassPaht)目錄下的類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中
自定義類(lèi)加載器:第四層加載器,由用戶編寫(xiě)的類(lèi)加載器
雙親委派模型的工作過(guò)程:
如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它不會(huì)自己先去加載這個(gè)類(lèi),而是把請(qǐng)求委派給父類(lèi)請(qǐng)求加載器完成,因此所有加載請(qǐng)求最終都會(huì)送到頂層的啟動(dòng)類(lèi)加載器,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求時(shí)(搜索范圍中沒(méi)有找到所需的類(lèi)),子加載器才會(huì)嘗試自己去加載。
總結(jié)
以上是生活随笔為你收集整理的JVM实战与原理---类加载机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java数据库篇7——数据库设计
- 下一篇: JVM实战与原理---字节码执行引擎