1. 類生命周期7個(gè)階段
類從被加載到虛擬機(jī)內(nèi)存中開始柳洋,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)叹坦、驗(yàn)證(Verification)熊镣、準(zhǔn)備(Preparation)、解析(Resolution)募书、 初始化(Initialization)绪囱、使用(Using)和卸載(Unloading)7個(gè)階段。其中驗(yàn)證莹捡、準(zhǔn)備鬼吵、解析3個(gè)部分統(tǒng)稱為連接(Linking)
2. 階段順序
加載、校驗(yàn)道盏、準(zhǔn)備而柑、初始化和卸載這五個(gè)階段的順序是確定的文捶,但是對(duì)于“解析”階段則不一定荷逞,它在某些情況下可以在初始化之后再開始媒咳,這樣做是 為了支持 java 的運(yùn)行時(shí)綁定特征(也稱為動(dòng)態(tài)綁定或晚期綁定)。
3. 加載的時(shí)機(jī)
什么是需要開始類第一個(gè)階段“加載”种远,虛擬機(jī)規(guī)范沒有強(qiáng)制約束涩澡,這點(diǎn)交給虛擬機(jī)的具體實(shí)現(xiàn)來自由把控。 “加載 loading”階段是整個(gè)類加載(class loading)過程的一個(gè)階段坠敷。
加載階段虛擬機(jī)需要完成以下3件事情:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流妙同。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象膝迎,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口粥帚。
注意:比如“通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”沒有指定一定得從某個(gè)class文件中獲取,所以我們可以從zip壓縮包限次、從網(wǎng)絡(luò)中 獲取芒涡、運(yùn)行時(shí)計(jì)算生成、數(shù)據(jù)庫中讀取卖漫、或者從加密文件中獲取等等费尽。
我們也可以通過前面的工具JHSD可以看到,JVM 啟動(dòng)后羊始,相關(guān)的類已經(jīng)加載進(jìn)入了方法區(qū)旱幼,成為了方法區(qū)的運(yùn)行時(shí)結(jié)構(gòu)。
-
Attarch上JVM啟動(dòng)的進(jìn)程
-
打開HSDB
- 可以看到很多class已經(jīng)被加載進(jìn)來了突委,找到JVMObject(這里已經(jīng)是內(nèi)存了柏卤,所以說相關(guān)的類已經(jīng)加載進(jìn)入了方法區(qū),成為了方法區(qū)的運(yùn)行時(shí)結(jié)構(gòu))
4.驗(yàn)證
驗(yàn)證是連接階段的第一步匀油,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求缘缚,并且不會(huì)危害虛擬機(jī)自身的安全。但從整體 上看钧唐,驗(yàn)證階段大致上會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證忙灼、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證钝侠、符號(hào)引用驗(yàn)證.
4.1 文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范该园,并且能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括下面這些驗(yàn)證點(diǎn):
- 是否以魔數(shù)OxCAFEBABE開頭帅韧。
- 主里初、次版本號(hào)是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)。
- 常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)忽舟。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量双妨。
- CONSTANT Utf8 info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)淮阐。
- Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息。
......
以上的部分還只是一小部分刁品,沒必要進(jìn)行深入的研究
這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的泣特,只有通過了這個(gè)階段的驗(yàn)證之后,這段字節(jié)流才被允許進(jìn)人Java虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)挑随,所以后面 的三個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)(內(nèi)存)上進(jìn)行的状您,不會(huì)再直接讀取、操作字節(jié)流了
4.2 元數(shù)據(jù)驗(yàn)證
第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語義分析兜挨,以保證其描述的信息符合《Java 語言規(guī)范》的要求膏孟,這個(gè)階段可能包括的驗(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ī)則的方法重載傅联,例如方法參數(shù)都-致先改,但返回值類型卻不同等)。
......
以上的部分還只是一小部分蒸走,沒必要進(jìn)行深入的研究仇奶。
元數(shù)據(jù)驗(yàn)證是驗(yàn)證的第二階段,主要目的是對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn)比驻,保證不存在與《Java語言規(guī)范》定義相悖的元數(shù)據(jù)信息该溯。
4.3 字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一一個(gè)階段, 主要目的是通過數(shù)據(jù)流分析和控制流分析别惦,確定程序語義是合法的狈茉、符合邏輯的。在第二 階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗(yàn)完畢以后掸掸,這階段就要對(duì)類的方法體(Class文件中的Code屬性)進(jìn)行校驗(yàn)分析氯庆,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做 出危害虛擬機(jī)安全的行為,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作扰付,例如不會(huì)出現(xiàn)類似于“在操作棧放置了一個(gè)int類型的數(shù)據(jù)堤撵,使用時(shí)卻按long類型 來加載入本地變量表中”這樣的情況。
- 保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上羽莺。
- 保證方法體中的類型轉(zhuǎn)換總是有效的实昨,例如可以把-個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,這是安全的盐固,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型荒给,甚至把對(duì) 象賦值給與它毫無繼承關(guān)系丈挟、完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的志电。
......
以上的部分還只是一小部分曙咽,沒必要進(jìn)行深入的研究。
如果一個(gè)類型中有方法體的字節(jié)碼沒有通過字節(jié)碼驗(yàn)證溪北,那它肯定是有問題的
4.4 符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)行為發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候桐绒,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段一解析階段中發(fā)生夺脾。符號(hào)引用驗(yàn)證可以看 作是對(duì)類自身以外(常量池中的各種符號(hào)引用)的各類信息進(jìn)行匹配性校驗(yàn)之拨,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類咧叭、方法蚀乔、字段等資源。本階段通常需要校驗(yàn)下列內(nèi)容:
- 符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類菲茬。
- 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段吉挣。
- 符號(hào)引用中的類、字段婉弹、方法的可訪問性( private睬魂、 protected. public艇搀、 <package> )
- 是否可被當(dāng)前類訪問唯卖。
......
符號(hào)引用驗(yàn)證的主要目的是確保解析行為能正常執(zhí)行脏里,如果無法通過符號(hào)引用驗(yàn)證屿衅,將會(huì)拋出異常
驗(yàn)證(總結(jié))
驗(yàn)證階段對(duì)于虛擬機(jī)的類加載機(jī)制來說泊脐,是一個(gè)非常重要的愁茁、 但卻不是必須要執(zhí)行的階段疼燥,因?yàn)轵?yàn)證階段只有通過或者不通過的差別件余,只要通過了驗(yàn)證良姆, 其后就對(duì)程序運(yùn)行期沒有任何影響了肠虽。如果程序運(yùn)行的全部代碼(包括自己編寫的、第三方包中的玛追、從外部加載的税课、動(dòng)態(tài)生成的等所有代碼)都已經(jīng)被反復(fù) 使用和驗(yàn)證過,在生產(chǎn)環(huán)境的實(shí)施階段就可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施痊剖,以縮短虛擬機(jī)類加載的時(shí)間韩玩。
5. 準(zhǔn)備
準(zhǔn)備階段是正式為類中定義的變量(被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配邢笙。
這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下:
首先啸如,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量氮惯,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中叮雳。
其次想暗,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量的定義為:
public static int value=123帘不;
那變量value在準(zhǔn)備階段過后的初始值為0而不是 123说莫,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何 Java方法,而把value賦值為123是后續(xù)的初始化環(huán)節(jié).
6. 解析
解析階段是JVM將常量池內(nèi)的符號(hào)引用替換為直接引用的過程寞焙。
符號(hào)引用是一種定義储狭,可以是任何字面上的含義,而直接引用就是直接指向目標(biāo)的指針捣郊、相對(duì)偏移量辽狈。
直接引用的對(duì)象都存在于內(nèi)存中,你可以把通訊錄里的女友手機(jī)號(hào)碼呛牲,類比為符號(hào)引用刮萌,把面對(duì)面和你吃飯的女朋友,類比為直接引用娘扩。 解析大體可以分為:
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
我們了解幾個(gè)經(jīng)常發(fā)生的異常着茸,就與這個(gè)階段有關(guān)。
java.lang.NoSuchFieldError根據(jù)繼承關(guān)系從下往上琐旁,找不到相關(guān)字段時(shí)的報(bào)錯(cuò)涮阔。(字段解析異常)
java.lang.IllegalAccessError字段或者方法,訪問權(quán)限不具備時(shí)的錯(cuò)誤灰殴。(類或接口的解析異常)
java.lang.NoSuchMethodError找不到相關(guān)方法時(shí)的錯(cuò)誤敬特。(類方法解析、接口方法解析時(shí)發(fā)生的異常)
7. 初始化
初始化主要是對(duì)一個(gè)class中的static{}語句進(jìn)行操作(對(duì)應(yīng)字節(jié)碼就是clinit方法)验懊。
< clinit >()方法對(duì)于類或接口來說并不是必需的擅羞,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量的賦值操作义图,那么編譯器可以不為這個(gè)類生成< clinit >()方法减俏。
初始化階段,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有6種情況必須立即對(duì)類進(jìn)行“初始化”(而加載碱工、驗(yàn)證娃承、準(zhǔn)備自然需要在此之前開始):
1)遇到new、getstatic怕篷、putstatic或invokestatic這4條字節(jié)碼指令時(shí)历筝,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化廊谓。生成這4條指令的最常見的Java代碼場景是:
- 使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候梳猪。
- 讀取或設(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)行過初始化呛哟,則需要先觸發(fā)其初始化。
3)當(dāng)初始化一個(gè)類的時(shí)候匿沛,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化扫责,則需要先觸發(fā)其父類的初始化。
4)當(dāng)虛擬機(jī)啟動(dòng)時(shí)逃呼,用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類)鳖孤,虛擬機(jī)會(huì)先初始化這個(gè)主類。
5)當(dāng)使用JDK1.7的動(dòng)態(tài)語言支持時(shí)抡笼,如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic苏揣、REF_putStatic、REF_invokeStatic的方法 句柄蔫缸,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化腿准,則需要先觸發(fā)其初始化。
6)當(dāng)一個(gè)接口中定義了JDK1.8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí)拾碌,如果這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前 被初始化街望。