類的整個生命周期的7個階段是:加載(Loading)湃密、驗證(Verification)畸悬、準備(Preparation)惫谤、解析(Resolution)、初始化(Initialization)际乘、使用(Using)、卸載(Unloading)漂佩。
類加載的全過程主要包括:加載脖含、驗證、準備仅仆、解析器赞、初始化這5個階段的內(nèi)容。
加載
加載是類加載過程的一個階段墓拜, 在加載階段JVM需要完成以下3件事情:
- 通過一個類的全限定明來獲取定義此類的二進制字節(jié)流港柜。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象咳榜,作為方法區(qū)這個類的各種數(shù)據(jù)訪問入口夏醉。
加載階段(準確地說,是加載階段獲取類的二進制字節(jié)流的動作)是整個類加載過程中開發(fā)人員可控性最強的涌韩,因為加載階段既可以使用系統(tǒng)提供的引導類加載器完成畔柔,又可以由用戶自定義的二類加載器去完成,開發(fā)人員可以通過定義自己的類加載器區(qū)控制字節(jié)流的獲取方式臣樱。
加載階段完成后靶擦,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)中腮考,方法區(qū)中的數(shù)據(jù)存儲格式由虛擬機的實現(xiàn)自行定義,虛擬機規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)玄捕。然后再內(nèi)存中實例化一個java.lang.Class類的對象(這個對象踩蔚,并沒有要求必須是在Java堆中,就HotSpot而言枚粘,Class對象比較特殊馅闽,雖然是對象,但是是存放在方法區(qū)中的)馍迄,這個對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口福也。
加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證東西)是交叉進行的,但是這兩個階段的開始時間仍然保持著固定的 先后順序攀圈。
驗證
驗證是連接階段的第一步暴凑,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全量承。驗證階段是非常重要的搬设,這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊撕捍,它大致上會完成4個階段的檢驗工作:文件格式驗證拿穴、元數(shù)據(jù)驗證、字節(jié)碼驗證忧风、符號引用驗證默色。
- 文件格式驗證
這一階段主要驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機處理狮腿。
驗證內(nèi)容包括:是否以魔數(shù)0xCAFEBABE開頭腿宰,主次版本號是否在當前虛擬機處理范圍之內(nèi),常量池的常量是否有不被支持的常量類型缘厢,指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量吃度,CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù),Class文件中各個部分及文件本身是否有被刪除的或附近的其他信息等等贴硫。
元數(shù)據(jù)驗證
- 第二階段主要是對類的元數(shù)據(jù)信息進行語義校驗椿每,保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。
驗證內(nèi)容包括:當前類是否有父類(除了Object類之外英遭,所有類都該有父類)间护,當前類的父類是否繼承了不被允許繼承的類(被final修飾的類),如果當前類不是抽象類挖诸,是否實現(xiàn)了其父類或接口之中要求實現(xiàn)的所有方法汁尺,類中的字段、方法是否與父類產(chǎn)生矛盾(如覆蓋了父類的final字段等)等等多律。
字節(jié)碼驗證
- 第三階段是整個驗證過程中最復雜的一個階段痴突,主要目的是通過數(shù)據(jù)流和控制流分析搂蜓,確定程序語義是合法的、符合邏輯的苞也。
驗證內(nèi)容包括:保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作洛勉,例如:保證不會出現(xiàn)在操作棧放置了一個int類型的數(shù)據(jù),使用時卻按long類型來加載如本地變量表中如迟。保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以為的字節(jié)碼指令上。保證方法體上的類型轉(zhuǎn)換是有效的攻走,例如:可以把一個子類對象賦值給父類數(shù)據(jù)類型殷勘,但是不能把父類對象賦值給子類數(shù)據(jù)類型。
符號引用驗證
- 第四個階段的校驗發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候昔搂,這個轉(zhuǎn)化動作發(fā)生在解析階段玲销。符號引用驗證可以看做是對類自身以外的信息進行匹配校驗。
驗證內(nèi)容包括:符號引用通過字符串描述的全限定明是否能找到對應的類摘符。在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段贤斜。符號引用中的類、字段逛裤、方法的訪問性是否可以被當前類訪問等等瘩绒。
準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配带族。這個階段分配內(nèi)存的僅僅是類變量不包括實例變量锁荔。實例變量實在對象實例化的時候分配在堆內(nèi)存中的,還有就是這里給類變量設(shè)置的初始值“通常情況下”下是數(shù)據(jù)類型的零值蝙砌,例如:
public static int value = 888;
這個變量value的值在準備階段被設(shè)置的初始值為0而不是666阳堕,因為此時尚未開始執(zhí)行任何Java方法,而把value賦值為666的putstatic指令是程序編譯后择克,存放于類構(gòu)造器<clinit>()方法之中恬总,所以把value賦值為666的動作將在初始化階段才會執(zhí)行。
上面說到在“通常情況”下初始值是零值肚邢,在非“通常情況”下也就是類字段屬性中存在常量屬性的時候壹堰,那么在準備階段類變量就會被初始化為常量屬性所指定的值。
public static final int value = 888;
編譯時Javac將會生成常量屬性道偷,在準備階段虛擬機就會根據(jù)常量屬性的設(shè)置將value賦值為666缀旁。
解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標勺鸦,符號可以是任何形式的字面量并巍,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)换途,引用目標并不一定已經(jīng)加載到內(nèi)存中懊渡。
直接引用(Direct References):直接引用可以是直接指向目標的指針刽射、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現(xiàn)內(nèi)存布局相關(guān)的剃执,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同誓禁。如果有直接引用,那引用的目標必定已經(jīng)在內(nèi)存中存在肾档。
解析動作主要針對類或接口摹恰、字段、類方法怒见、接口方法俗慈、方法類型、方法句柄和調(diào)用點限定符遣耍,這7類符號引用闺阱,分別對應于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info舵变、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 這7中常量類型酣溃。
初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中纪隙,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外赊豌,其余動作完全由虛擬機主導和控制。在準備階段瘫拣,變量已經(jīng)賦過一次系統(tǒng)初始零值了亿絮,而在初始化階段,是通過程序制定的主觀計劃去初始化類變量和其他資源麸拄,也就是執(zhí)行類構(gòu)造器<clinit>()方法的過程派昧。在上一篇“類的加載時機”中已經(jīng)介紹過了,有5中情況會出發(fā)類初始化拢切,下面介紹的是在<clinit>()方法執(zhí)行過程中一些可能會影響程序運行行為的特點和細節(jié)蒂萎。
- <clinit>()方法是由編譯器自動收集類中的所有類變量賦值動作和靜態(tài)語句塊(static{})中的語句合并產(chǎn)生的,編譯器收集順序室友語句在源文件中出現(xiàn)的豎線所決定的淮椰,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量五慈,定義在它之后的變量,在前面靜態(tài)語句塊可以賦值主穗,但是不能訪問泻拦。
- <clinit>()方法與類的構(gòu)造函數(shù)不同,它不需要顯示的調(diào)用父類構(gòu)造器忽媒,所以虛擬機中第一個被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object争拐。
由于父類的<clinit>()方法先執(zhí)行,也就意味著福利中定義的靜態(tài)語句塊要由于子類的變量賦值操作晦雨。 - <clinit>()方法對于類或接口來說并不是必需的架曹,如果一個類中沒有靜態(tài)語句塊隘冲,也沒有對變量的賦值操作,那么編譯器可以不為這個類生產(chǎn)<clinit>()方法绑雄。
接口中不能使用靜態(tài)語句塊展辞,但仍然有變量初始化的賦值操作,因此接口和類一樣都會生成<clinit>()方法万牺。接口中只有在使用父接口的時候才會初始化父接口(上一篇已經(jīng)講解過)罗珍。
虛擬機會保證一個類的<clinit>()方法在多線程的環(huán)境中被正確地枷鎖、同步脚粟,如果多個線程同時去初始化一個類靡砌,那么只會有一個線程區(qū)執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待珊楼,直到活動線程<clinit>()方法