類的生命周期如下圖所示
類加載的全過程包括加載巷帝,驗證匆笤,準(zhǔn)備研侣,解析,初始化這五個階段炮捧。
本篇文章我們來了解Java虛擬機中這五個階段的具體過程义辕。
加載(Loading)
“加載(Loading)”和“類加載(Class Loading)”是不同的兩個概念,前者是后者的一部分寓盗。在加載階段灌砖,虛擬機需要完成以下三個事情璧函。
①通過一個類的全限定名來獲取其定義的二進制字節(jié)流。
②將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)基显。
③在Java堆中生成一個代表這個類的 java.lang.Class對象蘸吓,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口。
加載階段可控性最強撩幽,在該階段库继,開發(fā)人員可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載窜醉。
加載階段與連接階段的部分內(nèi)容是交叉進行的宪萄,加載階段還沒完成,連接階段就可能已經(jīng)開始了榨惰,但是這兩個階段仍然保持固定的順序開始拜英。
連接(Linking)
驗證
驗證是連接階段的第一步,這一步的目的是為了保證Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求琅催,并且不會影響虛擬機自身的安全性居凶。
驗證階段大致會完成4個階段的檢驗動作:
①文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范,例如:是否以 0xCAFEBABE開頭藤抡、主次版本號是否在當(dāng)前虛擬機的處理范圍之內(nèi)侠碧、常量池中的常量是否有不被支持的類型(檢查常量tag標(biāo)志)、指向常量的各種索引值是否有指向不存在的變量或不符合類型的變量等等缠黍。
這個檢驗動作是基于二進制字節(jié)流進行的弄兜,目的是保證輸入的字節(jié)流能夠正確的被解析并存儲到方法區(qū)內(nèi)。
②元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析瓷式,以確保描述信息符合Java語言規(guī)范挨队,例如:除了 java.lang.Object之外,這個類是否有父類蒿往;該類的父類是否繼承了不可被繼承的類(final 類);若不是抽象類湿弦,那么是否全部實現(xiàn)父類或者接口中的方法等等瓤漏。
這個檢驗動作是基于方法區(qū)的存儲結(jié)構(gòu)進行的,目的對類的元數(shù)據(jù)信息進行語義檢驗颊埃,以符合Java語言規(guī)范蔬充。
③字節(jié)碼驗證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的班利、符合邏輯的饥漫。例如:保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作;保證方法體的類型轉(zhuǎn)換是有效的等等罗标。
這個檢驗動作是基于方法區(qū)的存儲結(jié)構(gòu)進行的庸队,該階段是最復(fù)雜的一個階段积蜻,對類的方法體進行分析校驗,保證虛擬機的安全性彻消。
④符號引用驗證:確保解析動作能正確執(zhí)行竿拆。發(fā)生在虛擬機將符號引用轉(zhuǎn)為直接引用的時候,這個動作其實是發(fā)生在解析階段宾尚。例如:能否通過類的全限定名找到對應(yīng)的類丙笋。
該驗證非常重要,但是不是必須的煌贴,如果所引用的類經(jīng)過反復(fù)驗證御板,那么可以考慮采用 -Xverifynone參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間牛郑。
準(zhǔn)備(Preparation)
準(zhǔn)備階段是正式為類變量在方法區(qū)分配內(nèi)存并設(shè)置類變量初始值的階段怠肋。
對于該階段有以下注意點:
①這時候進行內(nèi)存分配的僅包括類變量(static),而不包括實例變量井濒,實例變量會在對象實例化時隨著對象一塊分配在Java堆中灶似。
②這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認的零值(如0、0L瑞你、null酪惭、false等),而不是被在Java代碼中被顯式地賦予的值者甲。
public static int value=3春感;
變量value在準(zhǔn)備階段過后的初始值為0,而不是3虏缸,因為這時候尚未開始執(zhí)行任何Java方法鲫懒,而把value賦值為3的 publicstatic指令是在程序編譯后,存放于類構(gòu)造器 <clinit>()方法之中的刽辙,所以把value賦值為3的動作將在初始化階段才會執(zhí)行窥岩。
③對基本數(shù)據(jù)類型來說,對于類變量(static)和全局變量宰缤,如果不顯式地對其賦值而直接使用颂翼,則系統(tǒng)會為其賦予默認的零值,而對于局部變量來說慨灭,在使用前必須顯式地為其賦值朦乏,否則編譯時不通過。
④對于同時被static和final修飾的常量氧骤,必須在聲明的時候就為其顯式地賦值呻疹,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值筹陵,也可以在類初始化時顯式地為其賦值刽锤,總之镊尺,在使用前必須為其顯式地賦值,系統(tǒng)不會為其賦予默認零值姑蓝。
⑤對于引用數(shù)據(jù)類型reference來說鹅心,如數(shù)組引用、對象引用等纺荧,如果沒有對其進行顯式地賦值而直接使用旭愧,系統(tǒng)都會為其賦予默認的零值,即null宙暇。
⑥如果在數(shù)組初始化時沒有對數(shù)組中的各元素賦值输枯,那么其中的元素將根據(jù)對應(yīng)的數(shù)據(jù)類型而被賦予默認的零值。
⑦ 如果類字段的字段屬性表中存在 ConstantValue屬性占贫,即同時被final和static修飾桃熄,那么在準(zhǔn)備階段變量value就會被初始化為ConstValue屬性所指定的值。
假設(shè)上面的類變量value被定義為:
public static final int value=3型奥;
編譯時Javac將會為value生成ConstantValue屬性瞳收,在準(zhǔn)備階段虛擬機就會根據(jù) ConstantValue的設(shè)置將value賦值為3。我們可以理解為static final常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中
解析(Resolution)
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程厢汹,解析動作主要針對類或接口螟深、字段、類方法烫葬、接口方法界弧、方法類型、方法句柄和調(diào)用點限定符七類符號引用進行搭综。
關(guān)于符號引用與直接引用的解釋
符號引用:就是一組符號來描述目標(biāo)垢箕,可以是任何字面量,只要使用時能無歧義的定位到目標(biāo)就行兑巾,與虛擬機的內(nèi)存布局無關(guān)条获。
直接引用:直接指向目標(biāo)的指針萍虽、相對偏移量或一個間接定位到目標(biāo)的句柄觉壶,與虛擬機的內(nèi)存布局有關(guān)致盟。
初始化(Initialization)
類加載過程的最后一步糜颠,前面的類加載過程中,除了加載階段可以由用戶自定義類加載器參與外石挂,其余階段都完全由虛擬機主導(dǎo)和控制。在這個階段,才開始真正的執(zhí)行Java代碼称诗。
準(zhǔn)備階段會進行設(shè)置類變量初始值,在初始化階段头遭,會按照開發(fā)人員自己定義的數(shù)值去初始化類變量和其他資源寓免,也就是說初始化階段是執(zhí)行類構(gòu)造器<clinit>方法的過程癣诱。
區(qū)別于 <init>:其實就是構(gòu)造函數(shù),在生成class文件時袜香,編譯器會在構(gòu)造函數(shù)中添加一些代碼撕予,在本類的構(gòu)造函數(shù)中,會最優(yōu)先調(diào)用父類的<init>函數(shù)蜈首,接著執(zhí)行剩余構(gòu)造函數(shù)中剩余的代碼
什么叫<clinit>()方法與它的注意點
①<clinit>()方法是由編譯器自動收集類中所有的類變量的賦值動作和靜態(tài)語句塊(static{})中的語句合并而產(chǎn)生的实抡。編譯器的收集順序是由語句的出現(xiàn)順序決定,比如靜態(tài)語句塊(static{})只能訪問定義在它之前的變量欢策,定義在它之后的變量只能參與賦值吆寨,不能進行訪問。比如下面的代碼
class Demo{
static {
i = 0;//可以進行賦值
System.out.println(i); //這里無法編譯成功踩寇,提示“非法向前引用”
}
static int i = 0;
}
②<clinit>()方法與類的實例構(gòu)造函數(shù)<init>() 方法不同啄清,它不需要顯示的調(diào)用父類的構(gòu)造器,虛擬機會保證在子類的<clinit>()方法執(zhí)行之前俺孙,父類的<clinit>()方法已經(jīng)執(zhí)行完畢辣卒,因此第一個被虛擬機執(zhí)行的<clinit>()方法肯定是java.lang.object。
③由于父類的<clinit>()方法比子類的更先執(zhí)行完畢睛榄,因此父類的靜態(tài)語句塊要優(yōu)先于子類操作荣茫。
④并不是每個類或接口都必須有<clinit>()方法,如果這個類或接口中并沒有靜態(tài)語句塊懈费,也沒有對變量的賦值操作计露。
⑤接口與類一樣都會生成 <clinit>()方法,但是在接口中并不要求父接口全部都完成初始化憎乙,只有在真正使用到父接口的時候它才會被初始化(比如引用接口中定義的常量)票罐,另外接口的實現(xiàn)類在初始化時一樣不會先執(zhí)行接口的 <clinit>()方法。
⑥虛擬機會保證 <clinit>()方法在多線程環(huán)境下會被正確的加鎖泞边,同步该押。因此若一個類的 <clinit>()方法執(zhí)行時需要消耗大量時間,將會引起線程阻塞阵谚。在其他線程阻塞的情況下蚕礼,強制將占用 <clinit>()方法的線程退出,再將其他線程喚醒梢什,這些線程也不會再進入到 <clinit>()方法中奠蹬。同一個類加載器下,一個類型只會被初始化一次嗡午。
結(jié)束生命周期
在如下幾種情況下囤躁,Java虛擬機將結(jié)束生命周期
- 執(zhí)行了 System.exit()方法
- 程序正常執(zhí)行結(jié)束
- 程序在執(zhí)行過程中遇到了異常或錯誤而異常終止
- 由于操作系統(tǒng)出現(xiàn)錯誤而導(dǎo)致Java虛擬機進程終止
類的加載過程各階段對比