簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處夺荒,謝謝!
前言
Java源代碼被編譯成class字節(jié)碼良蒸,最終需要加載到虛擬機(jī)中才能運(yùn)行技扼。整個生命周期包括:加載、驗(yàn)證嫩痰、準(zhǔn)備剿吻、解析、初始化始赎、使用和卸載7個階段和橙。
加載
1、通過一個類的全限定名獲取描述此類的二進(jìn)制字節(jié)流造垛;
2魔招、將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)保存為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu);
3五辽、在java堆中生成一個代表這個類的java.lang.Class對象办斑,作為訪問方法區(qū)的入口;
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把加載動作放到JVM外部實(shí)現(xiàn)杆逗,以便讓應(yīng)用程序決定如何獲取所需的類乡翅,實(shí)現(xiàn)這個動作的代碼稱為“類加載器”,JVM提供了3種類加載器:
1罪郊、啟動類加載器(Bootstrap ClassLoader):負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的蠕蚜,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機(jī)認(rèn)可(按文件名識別悔橄,如rt.jar)的類靶累。
2、擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的癣疟,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫挣柬。
3、應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)加載用戶路徑(classpath)上的類庫睛挚。
JVM基于上述類加載器邪蛔,通過雙親委派模型進(jìn)行類的加載,當(dāng)然我們也可以通過繼承java.lang.ClassLoader實(shí)現(xiàn)自定義的類加載器扎狱。
雙親委派模型工作過程:當(dāng)一個類加載器收到類加載任務(wù)侧到,優(yōu)先交給其父類加載器去完成勃教,因此最終加載任務(wù)都會傳遞到頂層的啟動類加載器,只有當(dāng)父類加載器無法完成加載任務(wù)時床牧,才會嘗試執(zhí)行加載任務(wù)荣回。
雙親委派模型有什么好處?
比如位于rt.jar包中的類java.lang.Object戈咳,無論哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進(jìn)行加載壕吹,確保了Object類在各種加載器環(huán)境中都是同一個類著蛙。
驗(yàn)證
為了確保Class文件符合當(dāng)前虛擬機(jī)要求,需要對其字節(jié)流數(shù)據(jù)進(jìn)行驗(yàn)證耳贬,主要包括格式驗(yàn)證踏堡、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號引用驗(yàn)證咒劲。
- 格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合class文件格式的規(guī)范顷蟆,并且能被當(dāng)前虛擬機(jī)處理,如是否以魔數(shù)0xCAFEBABE開頭腐魂、主次版本號是否在當(dāng)前虛擬機(jī)處理范圍內(nèi)帐偎、常量池是否有不支持的常量類型等。只有經(jīng)過格式驗(yàn)證的字節(jié)流蛔屹,才會存儲到方法區(qū)的數(shù)據(jù)結(jié)構(gòu)削樊,剩余3個驗(yàn)證都基于方法區(qū)的數(shù)據(jù)進(jìn)行。
- 元數(shù)據(jù)驗(yàn)證
對字節(jié)碼描述的數(shù)據(jù)進(jìn)行語義分析兔毒,以保證符合Java語言規(guī)范漫贞,如是否繼承了final修飾的類、是否實(shí)現(xiàn)了父類的抽象方法育叁、是否覆蓋了父類的final方法或final字段等迅脐。 - 字節(jié)碼驗(yàn)證
對類的方法體進(jìn)行分析,確保在方法運(yùn)行時不會有危害虛擬機(jī)的事件發(fā)生豪嗽,如保證操作數(shù)棧的數(shù)據(jù)類型和指令代碼序列的匹配谴蔑、保證跳轉(zhuǎn)指令的正確性、保證類型轉(zhuǎn)換的有效性等昵骤。 - 符號引用驗(yàn)證
為了確保后續(xù)的解析動作能夠正常執(zhí)行树碱,對符號引用進(jìn)行驗(yàn)證,如通過字符串描述的全限定名是都能找到對應(yīng)的類变秦、在指定類中是否存在符合方法的字段描述符等成榜。
準(zhǔn)備
在準(zhǔn)備階段,為類變量(static修飾)在方法區(qū)中分配內(nèi)存并設(shè)置初始值蹦玫。
private static int var = 100;
準(zhǔn)備階段完成后赎婚,var 值為0刘绣,而不是100。在初始化階段挣输,才會把100賦值給val纬凤,但是有個特殊情況:
private static final int VAL= 100;
在編譯階段會為VAL生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)會根據(jù)ConstantValue屬性將VAL賦值為100撩嚼。
解析
解析階段是將常量池中的符號引用替換為直接引用的過程停士,符號引用和直接引用有什么不同?
1完丽、符號引用使用一組符號來描述所引用的目標(biāo)恋技,可以是任何形式的字面常量,定義在Class文件格式中逻族。
2蜻底、直接引用可以是直接指向目標(biāo)的指針、相對偏移量或則能間接定位到目標(biāo)的句柄聘鳞。
初始化
初始化階段是執(zhí)行類構(gòu)造器<clinit>方法的過程薄辅,<clinit>方法由類變量的賦值動作和靜態(tài)語句塊按照在源文件出現(xiàn)的順序合并而成,該合并操作由編譯器完成抠璃。
private static int value = 100;
static int a = 100;
static int b = 100;
static int c;
static {
c = a + b;
System.out.println("it only run once");
}
1站楚、<clinit>方法對于類或接口不是必須的,如果一個類中沒有靜態(tài)代碼塊鸡典,也沒有靜態(tài)變量的賦值操作源请,那么編譯器不會生成<clinit>;
2彻况、<clinit>方法與實(shí)例構(gòu)造器不同谁尸,不需要顯式的調(diào)用父類的<clinit>方法,虛擬機(jī)會保證父類的<clinit>優(yōu)先執(zhí)行纽甘;
3良蛮、為了防止多次執(zhí)行<clinit>驯击,虛擬機(jī)會確保<clinit>方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行弥鹦,如果有多個線程同時初始化一個類,那么只有一個線程能夠執(zhí)行<clinit>方法芍瑞,其它線程進(jìn)行阻塞等待左权,直到<clinit>執(zhí)行完成皮胡。
4、注意:執(zhí)行接口的<clinit>方法不需要先執(zhí)行父接口的<clinit>赏迟,只有使用父接口中定義的變量時屡贺,才會執(zhí)行。
類初始化場景
虛擬機(jī)中嚴(yán)格規(guī)定了有且只有5種情況必須對類進(jìn)行初始化。
- 執(zhí)行new甩栈、getstatic泻仙、putstatic和invokestatic指令;
- 使用reflect對類進(jìn)行反射調(diào)用量没;
- 初始化一個類的時候玉转,父類還沒有初始化,會事先初始化父類殴蹄;
- 啟動虛擬機(jī)時究抓,需要初始化包含main方法的類;
- 在JDK1.7中袭灯,如果java.lang.invoke.MethodHandler實(shí)例最后的解析結(jié)果REF_getStatic漩蟆、REF_putStatic、REF_invokeStatic的方法句柄妓蛮,并且這個方法句柄對應(yīng)的類沒有進(jìn)行初始化;
以下幾種情況圾叼,不會觸發(fā)類初始化
1蛤克、通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化夷蚊,而不會觸發(fā)子類的初始化构挤。
class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {
static {
System.out.println("child init惕鼓!");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Child.a);
}
}
輸出結(jié)果為:
parent init筋现!
100
2、定義對象數(shù)組箱歧,不會觸發(fā)該類的初始化矾飞。
public class Init{
public static void main(String[] args){
Parent[] parents = new Parent[10];
}
}
無輸出,說明沒有觸發(fā)類Parent的初始化呀邢,但是這段代碼做了什么洒沦?先看看生成的字節(jié)碼指令
anewarray指令為新數(shù)組分配空間,并觸發(fā)[Lcom.ctrip.ttd.whywhy.Parent類的初始化价淌,這個類由虛擬機(jī)自動生成申眼。
3、常量在編譯期間會存入調(diào)用類的常量池中蝉衣,本質(zhì)上并沒有直接引用定義常量的類括尸,不會觸發(fā)定義常量所在的類。
class Const {
static final int A = 100;
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Const.A);
}
}
輸出:
100
說明沒有觸發(fā)類Const的初始化病毡,在編譯階段濒翻,Const類中常量A的值100存儲到Init類的常量池中,這兩個類在編譯成class文件之后就沒有聯(lián)系了。
4肴焊、通過類名獲取Class對象前联,不會觸發(fā)類的初始化。
public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class c_dog = Dog.class;
Class clazz = Class.forName("zzzzzz.Cat");
}
}
class Cat {
private String name;
private int age;
static {
System.out.println("Cat is load");
}
}
class Dog {
private String name;
private int age;
static {
System.out.println("Dog is load");
}
}
執(zhí)行結(jié)果:Cat is load娶眷,所以通過Dog.class并不會觸發(fā)Dog類的初始化動作似嗤。
5、通過Class.forName加載指定類時届宠,如果指定參數(shù)initialize為false時烁落,也不會觸發(fā)類初始化,其實(shí)這個參數(shù)是告訴虛擬機(jī)豌注,是否要對類進(jìn)行初始化伤塌。
public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz = Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());
}
}
class Cat {
private String name;
private int age;
static {
System.out.println("Cat is load");
}
}
6、通過ClassLoader默認(rèn)的loadClass方法轧铁,也不會觸發(fā)初始化動作
new ClassLoader(){}.loadClass("zzzzzz.Cat");
END每聪。
我是占小狼。
在魔都艱苦奮斗齿风,白天是上班族药薯,晚上是知識服務(wù)工作者。
如果讀完覺得有收獲的話救斑,記得關(guān)注和點(diǎn)贊哦童本。
非要打賞的話,我也是不會拒絕的脸候。