一、加載
目的
加載(Loading)階段是整個(gè)類加載過(guò)程中的第一個(gè)階段流椒,需要完成以下三件事情:
- 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流敏簿。可通過(guò)以下方式實(shí)現(xiàn):
- ZIP 壓縮包中讀取宣虾,最終稱為 JAR惯裕、EAR、WAR 格式的基礎(chǔ)
- 網(wǎng)絡(luò)中獲取绣硝,最典型應(yīng)用:Web Applet
- 運(yùn)行時(shí)計(jì)算生成蜻势,多用在動(dòng)態(tài)代理技術(shù)
- 數(shù)據(jù)庫(kù)中讀取
- JSP文件生成
- 加密文件中獲取,典型的防 Class 文件被反編譯的保護(hù)措施
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在方法區(qū)內(nèi)存中生成一個(gè)代表這個(gè)類的
java.lang.Class
對(duì)象鹉胖,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)訪問(wèn)入口握玛。
非數(shù)組類型的加載
相對(duì)于其他階段,非數(shù)組類型的加載階段(加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的階段甫菠。加載階段既可以使用虛擬機(jī)里內(nèi)置的引導(dǎo)類加載器來(lái)完成挠铲,也可以由用戶自定義的類加載器去完成。
數(shù)組類型的加載
數(shù)組類本身不通過(guò)類加載器創(chuàng)建寂诱,它是由 Java 虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來(lái)的拂苹。但數(shù)據(jù)類的元素類型(Element Type),即數(shù)組去掉所有維度的類型刹衫,最終還是要靠類加載器來(lái)完成加載醋寝。
一個(gè)數(shù)組類 C 創(chuàng)建過(guò)程遵循以下規(guī)則:
- 如果數(shù)組的組件類型(Component Type )搞挣,即數(shù)組去掉一個(gè)維度的類型(不同于元素類型)带迟,為引用類型音羞,那就遞歸采用加載(Loading)階段的三個(gè)過(guò)程去加載這個(gè)組件類型,同時(shí)數(shù)組 C 也會(huì)被標(biāo)記在加載此組件類型的類加載器的類名稱空間上仓犬。
- 如果數(shù)組的組件類型不是引用類型嗅绰,類如
int[]
數(shù)組的組件類型為 int,Java 虛擬機(jī)將會(huì)把數(shù)組 C 標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)搀继。 - 數(shù)組類的可訪問(wèn)性與它的組件類型的可訪問(wèn)性一致窘面,如果組件類型不是引用類型,它的數(shù)組類的可訪問(wèn)性將默認(rèn)為 public叽躯,可被所有的類和接口訪問(wèn)到财边。
存儲(chǔ)位置
加載階段結(jié)束后,Java 虛擬機(jī)外部的二進(jìn)制流就按照虛擬機(jī)所設(shè)定的格式存儲(chǔ)在方法區(qū)中了点骑,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式完全由各虛擬機(jī)自行定義酣难。
類型數(shù)據(jù)安置在方法區(qū)中后,會(huì)在 Java 堆內(nèi)存中實(shí)例化一個(gè) java.lang.Class
類的對(duì)象黑滴,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)的類型數(shù)據(jù)的外部接口憨募。
二、驗(yàn)證
驗(yàn)證是連接階段的第一步袁辈。
目的
- 確保 Class 文件的字節(jié)流中包含的信息符合《Java 虛擬機(jī)規(guī)范》的全部要求菜谣。
- 保證字節(jié)碼編譯運(yùn)行后不會(huì)危害到虛擬機(jī)的自身安全。由于 Class 文件可以由任何途徑產(chǎn)生晚缩,甚至在二進(jìn)制編輯器中完成編寫(xiě)尾膊,故虛擬機(jī)需對(duì)這些字節(jié)碼進(jìn)行嚴(yán)格的校驗(yàn)。
驗(yàn)證階段包含大量驗(yàn)證荞彼,從整體上看冈敛,大致會(huì)完成四個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證卿泽、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證莺债。
文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合 Class 文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理签夭。
在 HotSpot 中這一階段可能包括以下驗(yàn)證點(diǎn):
- 是否以魔數(shù)
0xCAFEBABE
開(kāi)頭 - 主齐邦、次版本號(hào)是否在當(dāng)前 Java 虛擬機(jī)接受范圍內(nèi)
- 常量池的常量中是否由不被支持的常量類型
- CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 編碼的數(shù)據(jù)
- Class 文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息
......
此階段主要目的是 保證輸入的字節(jié)流能正確地解析并存儲(chǔ)在方法區(qū)。只有此階段驗(yàn)證通過(guò)后第租,字節(jié)流才被允許進(jìn)入 Java 虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)措拇,且后面三個(gè)階段全部是基于方法區(qū)存儲(chǔ)結(jié)構(gòu)上進(jìn)行。
元數(shù)據(jù)驗(yàn)證
對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析慎宾,以保證符合《Java 虛擬機(jī)規(guī)范》的全部要求丐吓。
在 HotSpot 中這一階段可能包括以下驗(yàn)證點(diǎn):
- 這個(gè)類是否有父類(除
java.lang.Object
外浅悉,所有類都應(yīng)當(dāng)有父類)。 - 這個(gè)類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)券犁。
- 如果這個(gè)類不是抽象類术健,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法。
- 類中的字段粘衬、方法是否與父類產(chǎn)生矛盾荞估,例如:覆蓋了父類的 final 字段、出現(xiàn)不符合規(guī)則的方法重載稚新。
......
此階段主要目的是 對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn)勘伺。
字節(jié)碼的驗(yàn)證
四個(gè)過(guò)程中最復(fù)雜的一個(gè)階段,通過(guò)數(shù)據(jù)流分析和控制流分析褂删,確保語(yǔ)義是否合法且符合邏輯飞醉。
在 HotSpot 中這一階段可能包括以下驗(yàn)證點(diǎn):
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作。
例如:在操作數(shù)棧放置了一個(gè) int 類型的數(shù)據(jù)屯阀,使用時(shí)卻按 long 類型來(lái)加載入本地變量表缅帘。 - 保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。
- 保證方法體中的類型轉(zhuǎn)換總是有效的蹲盘。
例如:子類對(duì)象賦值給父類數(shù)據(jù)類型股毫,是安全的,但反過(guò)來(lái)則是危險(xiǎn)的召衔。甚至賦值到其他不相關(guān)數(shù)據(jù)類型铃诬,則是不合法的。
此階段主要目的是 確保語(yǔ)義是否合法且符合邏輯苍凛。
符號(hào)引用驗(yàn)證
發(fā)生在虛擬機(jī)將 符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候趣席,且在解析階段中發(fā)生孝扛。
符號(hào)引用驗(yàn)證可以看作是對(duì)類自身以外(常量池中的各種符號(hào)引用)的類信息進(jìn)行匹配性校驗(yàn)卷扮,即對(duì)類是否缺少或禁止訪問(wèn)它依賴的某些外部類漱牵、方法是尔、字段等資源進(jìn)行校驗(yàn),無(wú)法通過(guò)驗(yàn)證則拋出 java.lang.IllegalAcessError冷守、java.lang.IllegalAcessError茂嗓、java.lang.IllegalAcessError 等異常尘惧。
在 HotSpot 中這一階段可能包括以下驗(yàn)證點(diǎn):
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類惭适。
- 在指定類中是否存在符合方法的字段描述符及簡(jiǎn)單名稱所描述的方法和字段笙瑟。
- 符號(hào)引用中的類、字段癞志、方法的可訪問(wèn)性(private往枷、protected、public、<package>)是否可被當(dāng)前類訪問(wèn)错洁。
......
此階段主要目的是 確保解析行為能正常進(jìn)行秉宿。
三、準(zhǔn)備
正式為類中定義的變量屯碴,即靜態(tài)變量描睦,進(jìn)行內(nèi)存分配并設(shè)置類變量初始值(零值)的階段。這些變量都存至方法區(qū)內(nèi)存中窿锉。
數(shù)據(jù)類型 | int | long | short | char | byte | boolean | float | double | reference |
---|---|---|---|---|---|---|---|---|---|
零值 | 0 | 0L | (short) 0 | '\u0000' | (byte) 0 | false | 0.0f | 0.0d | null |
方法區(qū)的定義:
在 JDK 7 及之前酌摇,HotSpot 使用永久代實(shí)現(xiàn)方法區(qū)膝舅。
在 JDK 8 及之后嗡载,永久代取消,方法存放于元空間(Metaspace)仍稀,類變量會(huì)隨著 Class 對(duì)象一起存放在 Java 堆中洼滚。元空間仍然與堆不相連,但與堆共享物理內(nèi)存技潘,邏輯上可認(rèn)為在堆中遥巴。
四、解析
Java 虛擬機(jī)將常量池內(nèi)的 符號(hào)引號(hào) 替換為 直接引用 的過(guò)程享幽。
解析發(fā)生的時(shí)間未進(jìn)行規(guī)定铲掐,只要求了在執(zhí)行 anewarray
、checkcast
值桩、getfield
摆霉、getstatic
、instanceof
奔坟、invokedynamic
携栋、invokeinterface
、invokespecial
咳秉、invokestatic
婉支、invokevirtual
、multianewarray
澜建、new
向挖、putfield
和 putstatic
等操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們所使用的符號(hào)引用進(jìn)行解析炕舵。
除 invokedynamic
外何之,虛擬機(jī)可對(duì)第一次解析結(jié)果進(jìn)行緩存,例如:運(yùn)行時(shí)直接引用常量池中的記錄幕侠。并把常量標(biāo)識(shí)為已解析狀態(tài)帝美,避免重復(fù)解析。
類加載過(guò)程包含:
1. 類或接口的解析
2. 字段的解析
3. 方法的解析
4. 接口方法的解析
五、初始化
類加載過(guò)程的最后一個(gè)步驟悼潭。至此庇忌,Java 虛擬機(jī)才真正開(kāi)始執(zhí)行類中編寫(xiě)的 Java 程序代碼,主導(dǎo)權(quán)移交給應(yīng)用程序舰褪。
準(zhǔn)備階段時(shí)皆疹,變量會(huì)賦零值,而在初始化階段占拍,則會(huì)根據(jù)開(kāi)發(fā)人員的主觀想法去進(jìn)行變量初始化略就,即為執(zhí)行 <clinit>()
方法的過(guò)程,<clinit>()
是由 Javac 編譯器自動(dòng)生成的晃酒。
特點(diǎn)
<clinit>()
方法是由編譯器自動(dòng)收集類的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}
塊)中的語(yǔ)句合并而成的表牢,收集順序由代碼順序決定。靜態(tài)語(yǔ)句塊只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量贝次,定義在它之后的變量崔兴,在前面的靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問(wèn)蛔翅。<clinit>()
方法與類的構(gòu)造函數(shù)(即實(shí)例構(gòu)造器 <init>() 方法)不同敲茄,它不需要顯示地調(diào)用父類構(gòu)造器,Java 虛擬機(jī)會(huì)保證在子類的<clinit>()
方法執(zhí)行前山析,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢堰燎。故 JVM 中第一個(gè)被執(zhí)行的<clinit>()
方法的類型肯定是java.lang.object
。由于父類的
<clinit>()
方法先執(zhí)行笋轨,意味著父類定義的靜態(tài)語(yǔ)句塊要優(yōu)于子類的變量賦值操作秆剪。<clinit>()
方法對(duì)于類和接口來(lái)說(shuō)并不是必需的,若沒(méi)有靜態(tài)語(yǔ)句塊翩腐,也沒(méi)變量賦值操作鸟款,可以不生成<clinit>()
方法。接口中不能使用靜態(tài)語(yǔ)句塊茂卦,但仍有變量初始化的賦值操作何什。因此接口與類一樣都會(huì)生成
<clinit>()
方法。但接口與類不同的是等龙,接口不需要先執(zhí)行父接口的<clinit>()
方法处渣,因?yàn)橹挥挟?dāng)父接口定義的變量被使用時(shí),父接口才會(huì)被初始化蛛砰。接口實(shí)現(xiàn)類初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()
方法罐栈。JVM 必須保證一個(gè)類的
<clinit>()
方法在多線程環(huán)境中被正確加鎖同步,如果多個(gè)線程同時(shí)初始化一個(gè)類泥畅,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()
方法荠诬,其他線程需要阻塞等待。