概述
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存殉了,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)互躬,轉(zhuǎn)換播赁,解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型吼渡,這就是Java的類加載機(jī)制容为。
在Java語(yǔ)言中,類的加載寺酪,連接和初始化過程都是在程序運(yùn)行期間完成的也就是動(dòng)態(tài)性坎背,雖然會(huì)增加類的加載性能開銷,但是這也為java應(yīng)用程序提供高度的靈活性
類加載的時(shí)機(jī)
類被加載到虛擬機(jī)內(nèi)存開始寄雀,到卸載出內(nèi)存為止得滤,生命周期包含: ** 加載,驗(yàn)證盒犹,準(zhǔn)備懂更,解析,初始化急膀,使用沮协,卸載 ** 7個(gè)階段,加載卓嫂,驗(yàn)證慷暂,準(zhǔn)備,初始化和卸載這5個(gè)順序是確定的命黔,解析階段則不一定呜呐,他在某些情況下可以在初始化階段之后在開始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定悍募。
初始化
Java虛擬機(jī)嚴(yán)格規(guī)定 ** 有且僅有 ** 五種情況必須立即對(duì)類進(jìn)行初始化
- 遇到new蘑辑,getstatic, putstatic坠宴, 或者 invokestatic 這4條字節(jié)碼指令時(shí)洋魂,如果沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。常見場(chǎng)景有使用new實(shí)例化對(duì)象副砍,讀取或設(shè)置一個(gè)類的靜態(tài)字段衔肢,調(diào)用一個(gè)類的靜態(tài)方法。
- 使用java.lang.reflect包的方法隊(duì)里進(jìn)行反射調(diào)用的時(shí)候豁翎,如果沒有進(jìn)行過初始化角骤,則需先觸發(fā)其初始化。
- 當(dāng)初始化一個(gè)類的時(shí)候心剥,發(fā)現(xiàn)其父類沒有進(jìn)行初始化邦尊,則先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)的時(shí)候优烧,用戶需要指定一個(gè)需要執(zhí)行的主類(包含main()方法的那個(gè)類)蝉揍,虛擬機(jī)會(huì)先初始化這個(gè)主類
- 當(dāng)使用JDK 1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后解析結(jié)果REF_getStatic畦娄,REF_putStatic又沾,REF_invokeStatic的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化熙卡,則需先要觸發(fā)其初始化杖刷。
** 如下代碼: 通過子類應(yīng)用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類被初始化**
public class SuperClass {
/**
* 被動(dòng)使用類字段
* 通過子類應(yīng)用父類的靜態(tài)字段再膳,不會(huì)導(dǎo)致子類被初始化
*/
static{
System.out.println("SuperClass init!");
}
public static int value = 100;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init!");
}
}
/**
* 非主動(dòng)使用類字段演示
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);
}
}
上面代碼運(yùn)行后只會(huì)輸出“SuperClass init! ”,對(duì)于靜態(tài)代碼字段挺勿,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過子類引用父類中靜態(tài)字段喂柒,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)之類的初始化不瓶。
** 通過數(shù)組定義來(lái)引用該類,不會(huì)導(dǎo)致此類的初始化 **
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
以上代碼不會(huì)有輸出
** 常量在編譯階段會(huì)存入調(diào)用類的常量池中本質(zhì)上沒有直接引用到定義常量的類灾杰,因此不會(huì)觸發(fā)定義常量類的初始化 **
public class ConstClass {
static {
System.out.println("ConstantClass init!");
}
public static final String HELLO = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);
}
}
以上只會(huì)輸出 hello world
類的加載過程
也就是 加載蚊丐,驗(yàn)證,準(zhǔn)備艳吠, 解析麦备, 初始化 這五個(gè)階段
1)加載
在加載階段,虛擬機(jī)主要完成以下3件事
- 通過一個(gè)類的全限定名來(lái)獲取定義此類的二進(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ù)訪問入口
加載階段完成后凛篙,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需要的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義栏渺,虛擬機(jī)規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)呛梆。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象,這個(gè)對(duì)象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口磕诊。
2)驗(yàn)證
驗(yàn)證時(shí)連接階段的第一步填物,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求纹腌,并且不會(huì)危害虛擬機(jī)自身的安全,主要包含以下內(nèi)容
- 文件格式驗(yàn)證:
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范滞磺,并且能被當(dāng)前版本的虛擬機(jī)處理包含以下內(nèi)容
- 是否以魔數(shù)0xCAFEBABE開頭
- 主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)
- 常量池的常量中是否有不被支持的常量類型
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
- CONSTANT_Utf8_info 型的常量中是否有不符合UFT8編碼的數(shù)據(jù)
- Class文件中各個(gè)部分及文件本身是否有本刪除的或附加的其他信息
·····
只有通過了這個(gè)階段的驗(yàn)證后升薯,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的击困,不會(huì)再直接操作字節(jié)流涎劈。
- 元數(shù)據(jù)驗(yàn)證
第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言的規(guī)范的要求沛励,包含以下信息
- 這個(gè)類是否有父類(除了java.lang.Object之外责语,所有的類都應(yīng)當(dāng)有父類)
- 這個(gè)類的父類是否繼承了不允許被繼承的類
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或者接口要求實(shí)現(xiàn)的所有的方法
- 類中的字段目派,方法是否與父類產(chǎn)生矛盾
······
第二階段主要目的是對(duì)類的元素?fù)?jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息
- 字節(jié)碼驗(yàn)證
第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段胁赢,主要目的是通過數(shù)據(jù)流和控制流分析企蹭,確定程序語(yǔ)法是否是合法的,符合邏輯的智末。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后谅摄,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)候不會(huì)做出對(duì)虛擬機(jī)有危害的事情系馆。包含如下內(nèi)容
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作送漠,例如不會(huì)出現(xiàn)類似這種情況:在操作棧中放置一個(gè)int類型數(shù)據(jù),使用卻將他按照l(shuí)ong類型使用
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 保證方法體中的類型轉(zhuǎn)換是有效的由蘑,例如可以把一個(gè)子類對(duì)象賦給父類數(shù)據(jù)類型闽寡,這是安全的,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型則是危險(xiǎn)的
······
- 符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候尼酿,這個(gè)轉(zhuǎn)換動(dòng)作發(fā)生在連接的第三階段--解析中發(fā)生爷狈,符號(hào)引用可以看做是對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn),通常需要校驗(yàn)一下內(nèi)容:
- 符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類
- 在指定的類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段
- 符號(hào)引用中的類裳擎,字段涎永,方法的訪問性是否可以被當(dāng)前類訪問。
3)準(zhǔn)備
準(zhǔn)備階段是正式為** 類變量 **分配內(nèi)存并設(shè)置變量初始值的階段鹿响,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配羡微,記住,只為類變量分配內(nèi)存惶我,不包括實(shí)例變量妈倔,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨對(duì)象一起分配在java堆中
如果類字段的字段屬性表中存在ConstantValue屬性,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值指孤,例如:
public static final int value = 123;//在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue設(shè)置將value賦值為123
4)解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程启涯,對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見的事情贬堵,虛擬機(jī)不會(huì)重新再解析而是通過緩存去拿出解析的數(shù)據(jù),但是invokedynamic指令除外结洼,它會(huì)每次被解析都會(huì)被重新解析黎做,解析動(dòng)作主要針對(duì)類,接口松忍,字段蒸殿,類方法,接口方法鸣峭,方法類型宏所,方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行,主要包含以下內(nèi)容
符號(hào)引用:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任意形式的字面量摊溶,只要使用時(shí)能夠無(wú)歧義的定位到目標(biāo)即可爬骤。
直接引用:直接引用可以是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄莫换。
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
5)初始化
類初始化階段是類加載過程的最后一步霞玄,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外拉岁,其余動(dòng)作全部由虛擬機(jī)主導(dǎo)和控制坷剧,到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼喊暖,在準(zhǔn)備階段變量已經(jīng)賦過一次系統(tǒng)要求的初始值惫企,而在初始化階段則通過程序制定的主觀計(jì)劃去初始化變量和其他資源,從另一個(gè)角度理解就是執(zhí)行類構(gòu)造器<clinit>()方法的過程
- <clinit>()方法是由編譯器自動(dòng)收集類中的所有變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的陵叽,他按照代碼中出現(xiàn)的順序收集狞尔,靜態(tài)語(yǔ)句塊中只能訪問到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在他之后的咨跌,在靜態(tài)語(yǔ)句塊中只能賦值不能訪問
public class Test {
static{
i = 1;//可以賦值
System.out.println(i);//不能訪問
}
static int i = 0;
}
- <clinit>()方法在執(zhí)行之前必須保證自己父類的類構(gòu)造器方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object
- 由于父類的<clinit>()方法優(yōu)先執(zhí)行沪么,意味著父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作
- <clinit>()并不是必須的,如果一個(gè)類中沒有靜態(tài)語(yǔ)句塊锌半,也沒有對(duì)變量的賦值操作禽车,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
- 接口中不能使用靜態(tài)語(yǔ)句塊刊殉,但仍然有變量初始化的賦值操作殉摔,因此接口與類一樣都會(huì)生成<clinit>()方法,但是接口與類不同的是记焊,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父類接口的<clinit>()方法逸月,只有父類接口中定義的變量使用時(shí)父類接口才會(huì)初始化,另外接口實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法
- 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確的加鎖遍膜,同步
類加載器
把類加載階段中“通過一個(gè)類的全限定名來(lái)獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到j(luò)ava虛擬機(jī)外部去實(shí)現(xiàn)碗硬,以便讓程序自己決定如何去獲取所需要的類瓤湘,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”
1)類與類的加載器
比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義恩尾,即使兩個(gè)類來(lái)源于同一個(gè)Class文件弛说,被同一個(gè)虛擬機(jī)加載,只要他們的類加載器不一樣翰意,那么這兩個(gè)類必定不相等(equals() isAssignableFrom() isInstance())
2)雙親委派模型
從java虛擬機(jī)的角度來(lái)講木人,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器,是虛擬機(jī)的一部分冀偶,另一種是所有其他的類加載器醒第,這些加載器由java語(yǔ)言實(shí)現(xiàn),獨(dú)立虛擬機(jī)之外进鸠,都繼承抽象類java.lang.ClassLoader
類加載器可以分為以下幾種
- 啟動(dòng)類加載器(Bootstrap ClassLoader)
- 擴(kuò)展類加載器(Extension ClassLoader)
- 應(yīng)用程序類加載器(Application ClassLoader):一般情況下這個(gè)是程序默認(rèn)的類加載器
** 以下是類加載器的雙親委派模型**
類加載器之間父子關(guān)系一般不會(huì)以繼承的關(guān)系來(lái)實(shí)現(xiàn)稠曼,都是使用組合關(guān)系來(lái)復(fù)用父加載器的代碼,如果一個(gè)類加載器收到類加載請(qǐng)求堤如,他首先不會(huì)自己去嘗試加載這個(gè)類蒲列,而是將這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次都是如此搀罢,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器,只有當(dāng)父類反饋無(wú)法完成這個(gè)加載請(qǐng)求時(shí)侥猩,子加載器才會(huì)嘗試自己去加載榔至。他的一個(gè)好處是Java類隨著他的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,例如java.lang.Object欺劳,他存放在rt.jar中唧取,無(wú)論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器進(jìn)行加載划提,因此Object類在程序的各種類加載環(huán)境中都是同一個(gè)類