一冒掌、類加載的時機
從類被加載到虛擬機內(nèi)存中開始淹父,到卸載出內(nèi)存為止株婴,它的整個生命周期分為7個階段,加載(Loading)暑认、驗證(Verification)困介、準備(Preparation)、解析(Resolution)蘸际、初始化(Initialization)座哩、使用(Using)、卸載(Unloading)粮彤。其中驗證八回、準備、解析三個部分統(tǒng)稱為連接驾诈。
7個階段發(fā)生的順序如下:
其中類加載的過程包括了加載、驗證溶浴、準備乍迄、解析、初始化五個階段士败。在這五個階段中闯两,加載、驗證谅将、準備和初始化這四個階段發(fā)生的順序是確定的漾狼,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始饥臂,這是為了支持Java語言的運行時綁定(也成為動態(tài)綁定或晚期綁定)逊躁。另外注意這里的幾個階段是按順序開始,而不是按順序進行或完成隅熙,因為這些階段通常都是互相交叉地混合進行的稽煤,通常在一個階段執(zhí)行的過程中調(diào)用或激活另一個階段。
對于類加載的時機囚戚,Java虛擬機規(guī)范中并沒有進行強制的約束酵熙,但是對于初始化階段,虛擬機規(guī)范嚴格規(guī)定了5種情況下必須對類進行初始化(類的加載驰坊、驗證匾二、準備就在這之前開始),這5中情況分別是:
- 遇到
new、getstatic察藐、putstatic或invokestatic
這四條字節(jié)碼指令時皮璧,如果類沒有進行過初始化,就需要進行類的初始化转培。這些場景包括:使用new關鍵字實例化對象時恶导、讀取或設置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)時浸须,以及調(diào)用一個類的靜態(tài)方法時惨寿; - 使用
java.lang.reflect
包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化删窒,就需要先進行類的初始化裂垦; - 當初始化一個類的時候,如果發(fā)現(xiàn)父類還沒有進行過初始化肌索,則需要先初始化父類蕉拢;
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main方法的那個類)诚亚,虛擬機會先初始化這個主類晕换;
- 當使用JDK 1.7的動態(tài)語言支持時,如果一個
java.lang.invoke.MethodHandle
實例最后的解析結(jié)果REF_getStatic站宗、REF_putStatic
闸准、REF_invokeStatic
的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化梢灭,則需要先觸發(fā)其初始化夷家;
這五種情況是Java虛擬機規(guī)范規(guī)定的主動初始化一個類的情況,除此之外的任何其他引用一個類的情況都不會主動初始化這個類敏释,這叫做被動引用库快。下面以三個例子說明什么是被動引用:
1、通過子類引用父類的靜態(tài)字段钥顽,并不會初始化子類
代碼如下:
package temp;
class SuperClass {
static{
System.out.println("Super Class init.");
}
public static int value=123;
}
public class SubClass extends SuperClass {
static{
System.out.println("Sub Class init.");
}
}
class Test1 {
@SuppressWarnings("unused")
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
結(jié)果為:
可以看到义屏,并沒有輸出SubClass init.
,對于靜態(tài)字段耳鸯,只有直接定義這個字段的類才會被初始化湿蛔。
2、通過數(shù)組定義來引用類县爬,不會觸發(fā)此類的初始化
代碼如下:
package temp;
class SuperClass {
static{
System.out.println("Super Class init.");
}
public static int value=123;
}
public class SubClass extends SuperClass {
static{
System.out.println("Sub Class init.");
}
}
class Test1 {
@SuppressWarnings("unused")
public static void main(String[] args) {
SuperClass[] sca=new SuperClass[10];
}
}
運行結(jié)果是什么也沒有打印阳啥。
3、調(diào)用類中的常量不會觸發(fā)該類的初始化
代碼如下:
package temp;
class ConstClass{
static{
System.out.println("ConstClass init.");
}
public static final String GREETING="Hello World.";
}
public class Test1{
public static void main(String[] args){
System.out.println(ConstClass.GREETING);
}
}
運行結(jié)果:
這里的ConstClass里定義了一個常量财喳,運行后也沒有ConstClass init.
輸出察迟,這時因為在編譯階段將這個常量的值存儲到了Test1類的常量池中斩狱,以后Test1類對這個常量的引用實際上都會被轉(zhuǎn)化為Test類對自身常量池的引用,即使在Java源碼中引用了ConstClass的GREETING常量扎瓶。也就是說所踊,實際上Test1的Class文件中并沒有對ConstClass類的符號引用入口,兩者在編譯成Class文件后就沒有關系了概荷。
接口的加載過程和類的加載過程有一些不同秕岛,不過接口也有初始化過程,上面的代碼都是用靜態(tài)代碼塊static{}
來輸出初始化信息的误证,而接口中不能使用static{}
語句塊继薛,但編譯器仍然會為接口生成<clinit()>
類構(gòu)造器,用于初始化接口中所定義的成員變量愈捅。接口與類真正有所區(qū)別的是在前面五種情況中的第三種:當一個類在初始化時遏考,它的所有父類都完成了初始化,但對一個接口初始化時蓝谨,并不要求所有的父接口都完成初始化灌具,只有在真正用藥父接口的時候才會初始化。
二譬巫、類加載過程
這里詳細講解類加載的全過程咖楣,也就是加載、驗證芦昔、準備截歉、解析和初始化五個階段所執(zhí)行的具體操作。
1烟零、加載階段
這在個階段,虛擬機要做的事情有如下三個:
- 通過一個類的全限定名來獲取定義這個類的二進制字節(jié)流咸作;
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)锨阿;
- 在內(nèi)存中生成一個代表這個類的
java.lang.Class
對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口记罚;
這里的二進制字節(jié)流并不一定要從Class文件獲取墅诡,只要一段二進制字節(jié)流符合Class文件的規(guī)范,都可以當做一個Class文件桐智,例如我們可以從jar包中獲取末早,從網(wǎng)絡中獲取,運行時生成等说庭,總之獲取Class文件的方式非常多然磷。
這里需要注意的是通過一個類的全限定名來獲取定義這個類的二進制字節(jié)流的動作是有類加載器完成的,對于非數(shù)組類的加載刊驴,我們可以通過自定義類加載器加載來進行控制姿搜。對于數(shù)組類就不行寡润,因為它不是通過類加載器創(chuàng)建的,而是由Java虛擬機直接創(chuàng)建的舅柜。
但數(shù)組類和非數(shù)組類也有很大的聯(lián)系梭纹,畢竟組成數(shù)組的元素就是非數(shù)組類(對于一維數(shù)組來說,而對于多維數(shù)組來說致份,可以遞歸加載)变抽,非數(shù)組類的創(chuàng)建需要類加載器完成。加載創(chuàng)建一個數(shù)組類的過程如下:
- 如果數(shù)組的元素類型是引用類型氮块,就遞歸加載這個元素類型绍载,這個數(shù)組將在加載該元素類型的類加載器的類名稱空間上被標識;
- 如果數(shù)組的元素類型不是引用類型(比如
int[]
數(shù)組)雇锡,Java虛擬機將會把數(shù)組標記為與引導類加載器關聯(lián)逛钻; - 數(shù)組類的可見性與元素類型的可見性一致,如果元素類型是不引用類型锰提,那么數(shù)組的可見性默認是
public
曙痘;
加載階段完成后,原來虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中立肘,不過方法區(qū)中的數(shù)據(jù)格式Java虛擬機規(guī)范沒有規(guī)定边坤。在這之后,虛擬機會在內(nèi)存中實例化一個java.lang.Class類的對象谅年。對于HotSpot虛擬機茧痒,雖然這是一個對象,按理說應該在Java堆中創(chuàng)建融蹂,不過HotSpot虛擬機是在方法區(qū)中創(chuàng)建的旺订。這個對象將作為程序訪問方法區(qū)中的這些數(shù)據(jù)類型的外部入口。
需要注意的是超燃,加載階段與連接階段的部分內(nèi)容是交叉進行的区拳。
2、驗證階段
驗證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當前虛擬機的要求意乓,而且不會危害虛擬機自身的安全樱调。不同的虛擬機對類驗證的實現(xiàn)可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證届良、元數(shù)據(jù)的驗證笆凌、字節(jié)碼驗證和符號引用驗證。
- 文件格式的驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范士葫,并且能被當前版本的虛擬機處理乞而,該驗證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)之內(nèi)。經(jīng)過該階段的驗證后慢显,字節(jié)流才會進入內(nèi)存的方法區(qū)中進行存儲晦闰,后面的三個驗證都是基于方法區(qū)的存儲結(jié)構(gòu)進行的放祟。
- 元數(shù)據(jù)驗證:對類的元數(shù)據(jù)信息進行語義校驗(其實就是對類中的各數(shù)據(jù)類型進行語法校驗),保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息呻右。
- 字節(jié)碼驗證:該階段驗證的主要工作是進行數(shù)據(jù)流和控制流分析跪妥,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為声滥。
- 符號引用驗證:這是最后一個階段的驗證眉撵,它發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候(解析階段中發(fā)生該轉(zhuǎn)化,后面會有講解)落塑,主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗纽疟。
3、準備階段
準備階段是正式為類變量分配內(nèi)存并設置類變量初始值的階段憾赁,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配污朽。這個階段中需要注意兩點,首先龙考,這個時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量)而不包括實例變量蟆肆,實例變量就在對象實例化時隨著對象一起分配在Java堆中。其次晦款,這里所說的初始值通常是數(shù)據(jù)類型的零值炎功,比如下面的類變量定義:
public static int value=10;
那么value在準備階段的初始值是0而不是10,因為這個時候還沒開始執(zhí)行任何Java方法缓溅,而將value賦值為10的putstatic指令是程序被編譯后蛇损,存放于類構(gòu)造器<clinit>()
方法中,所以把value賦值為10的動作將在初始化階段才會執(zhí)行坛怪。下標列出了所有基本類型的零值:
不過也有特殊情況淤齐,將類變量賦值為非零值。如果類字段的字段屬性表中存在ConstantValue屬性袜匿,那么在準備階段變量value就會被初始化為ConstantValue屬性所指定的值床玻,比如:
public static final int value=10;
編譯時Javac將會為value生成一個ConstantValue的屬性,在準備階段虛擬機就會根據(jù)ConstantValue的設置將value賦值為10沉帮。
4、解析階段
解析階段是虛擬機將常量池中的符號引用轉(zhuǎn)化為直接引用的過程贫堰。在Class類文件結(jié)構(gòu)一文中已經(jīng)比較過了符號引用和直接引用的區(qū)別和關聯(lián)穆壕,這里不再贅述。前面說解析階段可能開始于初始化之前其屏,也可能在初始化之后開始喇勋,虛擬機會根據(jù)需要來判斷,到底是在類被加載器加載時就對常量池中的符號引用進行解析(初始化之前)偎行,還是等到一個符號引用將要被使用前才去解析它(初始化之后)川背。
對同一個符號引用進行多次解析請求時很常見的事情贰拿,虛擬機實現(xiàn)可能會對第一次解析的結(jié)果進行緩存(在運行時常量池中記錄直接引用,并把常量標示為已解析狀態(tài))熄云,從而避免解析動作重復進行膨更。
解析動作主要針對類或接口、字段缴允、類方法荚守、接口方法四類符號引用進行,分別對應于常量池中的CONSTANT_Class_info
练般、CONSTANT_Fieldref_info
矗漾、CONSTANT_Methodref_info
、CONSTANT_InterfaceMethodref_info
四種常量類型薄料。
下面是四中符號符號引用的解析過程:
(1) 類或接口的解析
假設當前代碼處于類D中敞贡,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,虛擬機將經(jīng)歷如下的過程:
- 如果C不是一個數(shù)組類型摄职,那虛擬機將會把代表N的全限定名傳遞給D的類加載器區(qū)加載這個類誊役。在加載過程中,由于元數(shù)據(jù)驗證琳钉、字節(jié)碼驗證的需要势木,又可能觸發(fā)其他相關類的加載申尤,比如這個類的父類或?qū)崿F(xiàn)的接口离唬。如果加載過程出現(xiàn)異常,解析過程就失敗镣屹。
- 如果C是一個數(shù)組類型及皂,并且數(shù)組的元素類型是對象甫男,就會按照第一點加載數(shù)組元素類型。接著由虛擬機生成一個代表著數(shù)組維度和元素的數(shù)組對象验烧。
- 如果上面的步驟沒有異常板驳,那么C在虛擬機中實際上已經(jīng)成為一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證碍拆,確認D是否具備對C的訪問權(quán)限若治。
(2)字段解析
要解析一個未被解析過的字段符號引用,首先將會對字段表內(nèi)class_index
項中索引的CONSTANT_Class_info
符號引用進行解析感混,也就是字段所屬的類或借口的符號引用端幼。如果解析這個類或接口時發(fā)生異常,都會導致解析字段的失敗弧满。如果解析成功婆跑,才會繼續(xù)解析這個字段。具體的規(guī)則如下:
- 如果類或接口C本身就包含了簡單名稱和字段描述符都與目標匹配的字段庭呜,則返回這個字段的直接引用滑进,查找結(jié)束犀忱;
- 否則,如果在C中實現(xiàn)了接口扶关,將會按照繼承關系從下到上遞歸搜索各個接口和它的父接口阴汇,如果接口中包含了簡單名稱和字段描述符都與目標匹配的字段,則返回這個字段的直接引用驮审,查找結(jié)束鲫寄;
- 否則,如果C不是Object的話疯淫,將會按照繼承關系從下到上遞歸搜索父類地来,如果父類中包含了簡單名稱和字段描述符都與目標匹配的字段,返回這個字段的直接引用熙掺,查找結(jié)束未斑;
- 否則,查找失敗币绩。
- 之后蜡秽,還會對返回的字段進行權(quán)限驗證,如果不具備對字段的訪問權(quán)限缆镣,將拋出
java.lang.IllegalAccessError
異常芽突。
(3)類方法解析
類方法解析的第一個步驟和字段解析一樣,也需要先解析出類方法表的class_index
項中索引的方法所屬的類或接口的符號引用董瞻,如果解析成功寞蚌,按照如下步驟繼續(xù)(同樣以C來表示這個類):
- 類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發(fā)現(xiàn)class_index中索引的C是一個接口钠糊,直接失斝印;
- 然后抄伍,會在類C中查找這個方法艘刚;
- 否則,在類C的父類中遞歸查找這個方法截珍;
- 否則攀甚,在類C實現(xiàn)的接口列表中遞歸查找這個方法。如果找到一個匹配的方法岗喉,說明類C是一個抽象類秋度,查找結(jié)束,拋出
java.lang.AbstractMethodError
異常沈堡; - 否則,查找失敗燕雁,拋出
java.lang.NoSuchMethodError
異常诞丽。 - 同樣鲸拥,成功返回后還要進行權(quán)限驗證。
(4)接口方法解析
接口方法也要解析接口方法表的class_index
所屬的類或接口引用僧免,如果解析成功刑赶,用C表示這個類或接口,虛擬機按照如下的規(guī)則搜索:
- 與類方法解析不同懂衩,如果在接口方法表中發(fā)現(xiàn)
class_index
是一個類而不是接口撞叨,直接拋出java.lang.IncompatibleClassChangeError
異常; - 否則浊洞,在接口C中查找這個方法牵敷;
- 否則,在接口C的父接口中遞歸查找法希;
- 否則枷餐,查找失敗,拋出java.lang.NoSuchMethodError異常苫亦。
返回成功后不會驗證權(quán)限毛肋,因為接口的方法都是public的。
注意:如果你比較細心的話會發(fā)現(xiàn)上面中提到的字段屋剑、方法润匙、接口方法中都提到了一個class_index
屬性,可是在相應的"字段表唉匾,方法表孕讳、接口方法表"中并沒有class_index
這個屬性,我在網(wǎng)上找了很長時間肄鸽,終于解決了這個疑問卫病。首先這里提到的字段表、方法表典徘、接口方法表并不是字段表集合蟀苛、方法表集合中的字段表和方法表,而是指常量池中的CONSTANT_Fieldref_info
逮诲、CONSTANT_Methodref_info
帜平、CONSTANT_InterfaceMethodref_info
,在這三個常量項中都有兩個引用梅鹦,一個是class_index
:指向該字段(方法裆甩、接口方法)所屬的類(接口),另一個是name_and_type_index
:指向了該字段(方法齐唆、接口方法)的描述(等同于字段表集合中的描述)嗤栓,這里限于篇幅,不多做解釋,想要深入理解的可以參考:
5、初始化階段
類初始化階段是類加載過程的最后一步堪澎,前面的類加載過程中擂错,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制樱蛤。到了初始化階段钮呀,才真正開始執(zhí)行類中定義的Java程序。
前面已經(jīng)知道昨凡,在準備階段變量已經(jīng)賦值過一次系統(tǒng)初始值了爽醋,而在初始化階段,則會根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其它內(nèi)容土匀。即子房,初始化階段是執(zhí)行類構(gòu)造器<clinit>()
方法的過程。下面是<clinit>()
方法的特點:
-
<clinit>()
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的就轧,編譯器收集的順序由語句塊在源文件中的順序決定的证杭,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在之后的變量不能訪問妒御,但能賦值解愤。 -
<clinit>()
方法與類構(gòu)造器<init>()不同,它不需要顯式調(diào)用父類的<clinit>()
方法乎莉,虛擬機保證在子類<clinit>()
方法執(zhí)行之前送讲,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢。 - 由于父類的
<clinit>()
方法先執(zhí)行惋啃,意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作哼鬓。 -
<clinit>()
方法對于類或接口來說并不是必須的,如果一個類中沒有靜態(tài)語句塊边灭,也沒有對變臉的賦值操作异希,那么編譯器就不會生成<clinit>()
方法。 - 接口中不能使用靜態(tài)語句塊绒瘦,但仍然有變量初始化的賦值操作称簿,因此接口與類一樣都會生成
<clinit>()
方法。但接口與類不同的是惰帽,執(zhí)行接口的<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法憨降。即,只有使用一個接口中的變量時该酗,才會執(zhí)行這個接口的<clinit>()
方法授药。 - 虛擬機會保證一個類的
<clinit>()
方法在多線程環(huán)境下中被正確的加鎖、同步,如果多個線程同時去初始化一個類悔叽,那么只有一個線程會去執(zhí)行這個類的<clinit>()
方法航邢,其他線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>()
方法完畢骄蝇。
這里限于篇幅,把類加載機制中比較重要的最后一部分——類加載器放到下一篇文章中操骡。