類的整個(gè)生命周期包括加載肝集、驗(yàn)證、準(zhǔn)備蛛壳、解析杏瞻、初始化、使用和卸載7個(gè)階段衙荐,其中驗(yàn)證捞挥、準(zhǔn)備、解析這3個(gè)部分統(tǒng)稱為連接忧吟,如下圖所示砌函。
加載、驗(yàn)證溜族、準(zhǔn)備胸嘴、初始化和卸載這5個(gè)階段的順序是確定的,而解析階段則不一定斩祭,它在某些情況下可以在初始化階段之后開(kāi)始劣像,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定)。注意摧玫,這里的幾個(gè)階段是按順序開(kāi)始耳奕,而不是按順序進(jìn)行或完成的绑青,因?yàn)檫@些階段通常都是互相交叉地混合進(jìn)行的,通常在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用屋群、激活另外一個(gè)階段闸婴。
1. 加載
加載階段主要查找并加載類的二進(jìn)制數(shù)據(jù)。在該階段芍躏,虛擬機(jī)需要完成以下3件事情:
(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)入口庇楞。
相對(duì)于類加載的其他階段,一個(gè)非數(shù)組類的加載階段是開(kāi)發(fā)人員可控性最強(qiáng)的階段否纬,因?yàn)樵撾A段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成吕晌,也可以由用戶自定義的類加載器去完成,開(kāi)發(fā)人員可以通過(guò)定義自己的類加載器去控制字節(jié)流的獲取方式临燃。
對(duì)于數(shù)組類睛驳,其本身不通過(guò)類加載器創(chuàng)建,它是由Java虛擬機(jī)直接創(chuàng)建的膜廊。但數(shù)組類與類加載器仍然有很密切的關(guān)系乏沸,因?yàn)閿?shù)組類的元素類型最終靠類加載器去創(chuàng)建。一個(gè)數(shù)組類的創(chuàng)建過(guò)程遵循以下規(guī)則:
- 如果數(shù)組的組件類型是引用類型爪瓜,那就遞歸采用本節(jié)中定義的加載過(guò)程去加載這個(gè)組件類型蹬跃,數(shù)組類將在加載該組件類型的類加載器的類名稱空間上被標(biāo)識(shí)。
- 如果數(shù)組的組件類型不是引用類型(如int[]數(shù)組)钥勋,Java虛擬機(jī)將會(huì)把數(shù)組類標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)炬转。
- 數(shù)組類的可見(jiàn)性與它的組件類型的可見(jiàn)性一致辆苔,如果組件類型不是引用類型算灸,那數(shù)組類的可見(jiàn)性默認(rèn)為public。
加載階段完成后驻啤,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中菲驴,然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class
類的對(duì)象,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的這些數(shù)據(jù)的外部接口骑冗。
2. 驗(yàn)證
驗(yàn)證階段主要確保被加載的類的正確性赊瞬。這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全贼涩。驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:
(1)文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范巧涧,而且能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括下面這些驗(yàn)證點(diǎn):
- 是否以0xCAFEBABE開(kāi)頭遥倦。
- 主谤绳、次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)。
- 常量池中的常量是否有不被支持的常量類型。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量缩筛。
- Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息消略。
第一階段的驗(yàn)證點(diǎn)遠(yuǎn)不止這些,該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)內(nèi)瞎抛,格式上符合描述一個(gè)Java類型信息的要求艺演。該階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(gè)階段的驗(yàn)證后桐臊,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)胎撤,所以后面的3個(gè)驗(yàn)證階段都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流豪硅。
(2)元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析哩照,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn):
- 這個(gè)類是否有除了java.lang.Object之外的父類懒浮。
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)飘弧。
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法砚著。
- 類中的字段次伶、方法是否與父類產(chǎn)生矛盾。
(3)字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析稽穆,確定程序語(yǔ)義是合法的冠王、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后舌镶,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析柱彻,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作餐胀。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上哟楷。
- 保證方法體中的類型轉(zhuǎn)換是有效的。
(4)符號(hào)引用驗(yàn)證:最后一個(gè)階段的校驗(yàn)發(fā)生在解析階段否灾,其對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)卖擅,通常需要校驗(yàn)以下內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段墨技。
- 符號(hào)引用中的類惩阶、字段、方法的訪問(wèn)性是否可被當(dāng)前類訪問(wèn)扣汪。
符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行断楷,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,那么將會(huì)拋出異常崭别。該階段是一個(gè)非常重要的冬筒、但不是一定必要的階段统刮。
3. 準(zhǔn)備
準(zhǔn)備階段主要為類的靜態(tài)變量分配內(nèi)存并將其初始化為默認(rèn)值,這些內(nèi)存都將在方法區(qū)中分配账千。
該階段有兩點(diǎn)需要注意:
(1)首先侥蒙,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量匀奏,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中鞭衩。
(2)其次,這里所設(shè)置的初始值通常是數(shù)據(jù)類型默認(rèn)的初始值娃善,而不是被在Java代碼中被顯式地賦予的值论衍。這里還需要注意如下幾點(diǎn):
- 對(duì)基本數(shù)據(jù)類型來(lái)說(shuō),對(duì)于類變量(static)和全局變量聚磺,如果不顯式地對(duì)其賦值而直接使用坯台,則系統(tǒng)會(huì)為其賦予默認(rèn)的零值,而對(duì)于局部變量來(lái)說(shuō)瘫寝,在使用前必須顯式地為其賦值蜒蕾,否則編譯時(shí)不通過(guò)。
- 對(duì)于同時(shí)被
static
和final
修飾的常量焕阿,必須在聲明的時(shí)候就為其顯式地賦值咪啡,否則編譯時(shí)不通過(guò);而只被final修飾的常量則既可以在聲明時(shí)顯式地為其賦值暮屡,也可以在類初始化時(shí)顯式地為其賦值撤摸,總之,在使用前必須為其顯式地賦值褒纲,系統(tǒng)不會(huì)為其賦予默認(rèn)零值准夷。 - 對(duì)于引用數(shù)據(jù)類型來(lái)說(shuō),如數(shù)組引用莺掠、對(duì)象引用等衫嵌,如果沒(méi)有對(duì)其進(jìn)行顯式地賦值而直接使用,系統(tǒng)都會(huì)為其賦予默認(rèn)的零值汁蝶,即
null
渐扮。 - 如果在數(shù)組初始化時(shí)沒(méi)有對(duì)數(shù)組中的各元素賦值论悴,那么其中的元素將根據(jù)對(duì)應(yīng)的數(shù)據(jù)類型而被賦予默認(rèn)的零值掖棉。
如果類字段的字段屬性表中存在ConstantValue屬性(同時(shí)被final和static修飾),那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值膀估,假設(shè)類變量value被定義為:
public static final int value = 123;
編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性幔亥,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。
4. 解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程察纯。符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo)帕棉,可以是任何字面量针肥;直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄香伴。解析動(dòng)作主要針對(duì)類或接口慰枕、字段、類方法即纲、接口方法具帮、方法類型、方法句柄和調(diào)用點(diǎn)限定符這7類符號(hào)引用進(jìn)行低斋。下面將講解前面4種引用的解析過(guò)程蜂厅。
- 類或接口的解析
假設(shè)當(dāng)前代碼所處的類為D,如果要把一個(gè)從未解析過(guò)的符號(hào)引用N解析為一個(gè)類或接口C的直接引用膊畴,虛擬機(jī)需要以下3個(gè)步驟:
(1)如果C不是一個(gè)數(shù)組類型掘猿,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給D的類加載器去加載這個(gè)類C。
(2)如果C是一個(gè)數(shù)組類型唇跨,并且數(shù)組的元素類型為對(duì)象稠通,那將會(huì)按照第1點(diǎn)的規(guī)則加載數(shù)組元素類型。
(3)如果上面的步驟沒(méi)有出現(xiàn)任何異常采记,那么C在虛擬機(jī)中實(shí)際上已經(jīng)成為一個(gè)有效的類或接口了,但在解析完成之前還要進(jìn)行符號(hào)引用驗(yàn)證政勃,確認(rèn)D是否具備對(duì)C的訪問(wèn)權(quán)限唧龄。
- 字段解析
要解析一個(gè)未被解析過(guò)的字段符號(hào)引用,首先將會(huì)對(duì)字段所屬的類或接口的符號(hào)引用進(jìn)行解析奸远。如果在解析這個(gè)類或接口符號(hào)引用的過(guò)程中出現(xiàn)了任何異常既棺,都會(huì)導(dǎo)致字段符號(hào)引用解析的失敗。如果解析成功完成懒叛,那將這個(gè)字段所屬的類或接口用C表示丸冕,虛擬機(jī)規(guī)范要求按照如下步驟對(duì)C進(jìn)行后續(xù)字段的搜索:
(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)系從下往上遞歸搜索其父類赋秀,如果在父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段利朵,則返回這個(gè)字段的直接引用,查找結(jié)束猎莲。
(4)否則绍弟,查找失敗,拋出java.lang.NoSuchFieldError異常著洼。
最后晌柬,如果查找過(guò)程成功返回了直接引用,就會(huì)對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證年碘,如果發(fā)現(xiàn)不具備對(duì)字段的訪問(wèn)權(quán)限,將拋出java.lang.IllegalAccessError異常展鸡。
- 類方法解析
類方法解析也需要先解析出類方法所屬的類或接口的符號(hào)引用屿衅,如果解析成功,依然用C表示這個(gè)類莹弊,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的類方法搜索:
(1)類方法和接口方法符號(hào)引用的常量類型定義是分開(kāi)的涤久,如果在類方法表中發(fā)現(xiàn)C是個(gè)接口,直接拋出異常忍弛。
(2)否則响迂,在類C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用细疚,查找結(jié)束蔗彤。
(3)否則,在類C的父類中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法疯兼,如果有則返回這個(gè)方法的直接引用然遏,查找結(jié)束。
(4)否則吧彪,在類C實(shí)現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法待侵,如果存在匹配的方法,說(shuō)明類C是一個(gè)抽象類姨裸,查找結(jié)束秧倾,拋出異常。
(5)否則傀缩,宣告方法查找失敗审磁,拋出java.lang.NoSuchMethodError
異常牵舵。
最后昌讲,如果查找過(guò)程成功返回了直接引用诗舰,就會(huì)對(duì)這個(gè)方法進(jìn)行權(quán)限驗(yàn)證俘枫,如果發(fā)現(xiàn)不具備對(duì)此方法的訪問(wèn)權(quán)限,將拋出java.lang.IllegalAccessError
異常箱季。
- 接口方法解析
接口方法解析也需要先解析出接口方法所屬的類或接口的符號(hào)引用裸删,如果解析成功,依然用C表示這個(gè)接口换帜,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的接口方法搜索:
(1)如果在接口方法表中發(fā)現(xiàn)C是個(gè)類而不是接口楔壤,直接拋出異常。
(2)否則惯驼,在接口C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法蹲嚣,如果有則返回這個(gè)方法的直接引用,查找結(jié)束祟牲。
(3)否則隙畜,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類為止说贝,看是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法议惰,如果有則返回這個(gè)方法的直接引用,查找結(jié)束乡恕。
(4)否則言询,宣告方法查找失敗,拋出java.lang.NoSuchMethodError
異常傲宜。
由于接口中的所有方法默認(rèn)都是public
的运杭,所以不存在訪問(wèn)權(quán)限的問(wèn)題。
5. 初始化
初始化是指為類的靜態(tài)變量賦予正確的初始值函卒,JVM負(fù)責(zé)對(duì)類進(jìn)行初始化辆憔,主要對(duì)類變量進(jìn)行初始化。在Java中對(duì)類變量進(jìn)行初始值設(shè)定有兩種方式:
(1)聲明類變量時(shí)指定初始值报嵌;
(2)使用靜態(tài)代碼塊為類變量指定初始值躁愿。
JVM初始化步驟:
(1)假如這個(gè)類還沒(méi)有被加載和連接,則程序先加載并連接該類沪蓬;
(2)假如該類的直接父類還沒(méi)有被初始化彤钟,則先初始化其直接父類;
(3)假如類中有初始化語(yǔ)句跷叉,則系統(tǒng)依次執(zhí)行這些初始化語(yǔ)句逸雹。
類初始化時(shí)機(jī):只有當(dāng)對(duì)類主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類的初始化,類的主動(dòng)使用包括以下6種:
- 創(chuàng)建類的實(shí)例云挟,也就是new的方式梆砸;
- 訪問(wèn)某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值园欣;
- 調(diào)用類的靜態(tài)方法帖世;
- 反射(如Class.forName("…"));
- 初始化某個(gè)類的子類沸枯,則其父類也會(huì)被初始化日矫;
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類赂弓,直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類。
參考
- 《深入理解Java虛擬機(jī)(第2版)》
- (JVM)Java虛擬機(jī):類加載的5個(gè)過(guò)程