Java類加載機制
類編譯
類編譯,即 .java 文件通過javac命令編譯成 .class 文件,才能在虛擬機上正常運行代碼适袜≌懿矗可以通過 javap查看class文件包含信息剩蟀。
重點:編譯后的字節(jié)碼文件主要包括常量池和方法表集合這兩部分
-
常量池 :主要記錄的是字節(jié)碼文件中出現(xiàn)的字面量以及符號引用
字面常量包括字符串常量(例如 String str=“abc”,其中”abc”就是常量)切威,聲明為 final 的屬性以及一些基本類型(例如育特,范圍在 -127-128 之間的整型)的屬性。
符號引用包括類和接口的全限定名先朦、類引用缰冤、方法引用以及成員變量引用(例如 String str=“abc”犬缨,其中 str 就是成員變量引用)等。
-
方法表集合
- 包含一些方法的字節(jié)碼锋谐、方法訪問權(quán)限(public遍尺、protect、prviate 等)涮拗、方法名索引(與常量池中的方法引用對應(yīng))乾戏、描述符索引、JVM 執(zhí)行指令以及屬性集合等三热。
類加載流程
類從被加載到虛擬機內(nèi)存到卸出內(nèi)存為止鼓择,類加載的整個生命周期分為加載-連接-初始化三個階段。
1. 加載階段:
加載階段是指將字節(jié)碼數(shù)據(jù)(類的.class文件的二進制數(shù)據(jù))從不同的數(shù)據(jù)源讀取到 JVM內(nèi)存中就漾,放在方法區(qū)內(nèi)呐能,在堆中創(chuàng)建java.lang.Class對象,封裝方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)抑堡。
字節(jié)碼數(shù)據(jù)源可以來自于磁盤文件 *.class摆出,也可以是 jar 包里的 *.class,也可以來自遠程服務(wù)器提供的字節(jié)流首妖,字節(jié)碼的本質(zhì)就是一個字節(jié)數(shù)組 []byte偎漫,有特定的復雜的內(nèi)部格式。如果輸入數(shù)據(jù)不是 ClassFile 的結(jié)構(gòu)有缆,則會拋出 ClassFormatError象踊。
加載過程完成以下三步:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時存儲結(jié)構(gòu)棚壁。
- 在內(nèi)存中生成一個代表這個類的 Class 對象杯矩,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
加載階段即可以使用系統(tǒng)提供的類加載器在完成袖外,也可以由用戶自定義的類加載器來完成史隆。加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成曼验,連接階段可能已經(jīng)開始逆害。
2. 連接階段
2.1 驗證階段
確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全蚣驼。
整體分為4個階段的校驗工作:文件格式魄幕、元數(shù)據(jù)、字節(jié)碼颖杏、符號引用
2.2 準備階段
準備階段是正式為類變量(static 成員變量)分配內(nèi)存并設(shè)置類變量初始值(零值)的階段纯陨,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。
這時候進行內(nèi)存分配的僅包括類變量,而不包括實例變量翼抠,實例變量將會在對象實例化時隨著對象一起分配在堆中咙轩。
其次,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值阴颖,假設(shè)一個類變量的定義為:
public static int value = 123;
那么活喊,變量value在準備階段過后的值為0而不是123。因為這時候尚未開始執(zhí)行任何java方法量愧,而把value賦值為123的putstatic指令是程序被編譯后钾菊,存放于類構(gòu)造器方法之中,所以把value賦值為123的動作將在初始化階段才會執(zhí)行偎肃。
“特殊情況”:當類字段的字段屬性是常量時煞烫,會在準備階段初始化為指定的值,所以標注為final之后累颂,value的值在準備階段初始化為123而非0滞详。
2.3 解析階段
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。編譯時紊馏,Java 類并不知道所引用的類的實際地址料饥,因此只能使用符號引用來代替。類結(jié)構(gòu)文件的常量池中存儲了符號引用朱监,包括類和接口的全限定名稀火、類引用、方法引用以及成員變量引用等赌朋。如果要使用這些類和方法,就需要把它們轉(zhuǎn)化為 JVM 可以直接獲取的內(nèi)存地址或指針篇裁,即直接引用沛慢。
3. 初始化階段
類何時初始化?
虛擬機規(guī)范中并沒有強制約束何時進行加載达布,但是規(guī)范嚴格規(guī)定了有且只有下列五種情況必須對類進行初始化(加載团甲、驗證、準備都會隨著發(fā)生):
- 遇到 new黍聂、getstatic躺苦、putstatic、invokestatic 這四條字節(jié)碼指令時产还,如果類沒有進行過初始化匹厘,則必須先觸發(fā)其初始化。最常見的生成這 4 條指令的場景是:
a. 使用 new 關(guān)鍵字實例化對象的時候
b. 讀取或設(shè)置一個類的靜態(tài)字段的時候(被 final 修飾脐区、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)
c. 調(diào)用一個類的靜態(tài)方法的時候- 使用 java.lang.reflect 包的方法對類進行反射調(diào)用的時候愈诚,如果類沒有進行初始化,則需要先觸發(fā)其初始化。
- 當初始化一個類的時候炕柔,如果發(fā)現(xiàn)其父類還沒有進行過初始化酌泰,則需要先觸發(fā)其父類的初始化。
- 當虛擬機啟動時匕累,用戶需要指定一個要執(zhí)行的主類(包含 main() 方法的那個類)陵刹,虛擬機會先初始化這個主類;
- 當使用 JDK.7 的動態(tài)語言支持時欢嘿,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果為REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄衰琐,并且這個方法句柄所對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化际插;
init 和 clinit 方法區(qū)別碘耳?
- init是對象實例構(gòu)造器方法,即在程序執(zhí)行 new 一個對象調(diào)用該對象類的構(gòu)造方法時才會執(zhí)行init方法框弛,對非靜態(tài)變量解析初始化辛辨。
- clinit是類構(gòu)造器方法,在jvm類加載的加載-連接-初始化的初始化階段調(diào)用瑟枫。class類構(gòu)造器對靜態(tài)變量斗搞,靜態(tài)代碼塊進行初始化。
初始化階段才真正開始執(zhí)行類中的定義的 Java 程序代碼慷妙。初始化階段即虛擬機執(zhí)行類構(gòu)造器方法的過程僻焚。在準備階段,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值膝擂,而在初始化階段虑啤,根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其它資源。從另一個角度表達架馋,初始化階段即為執(zhí)行類構(gòu)造器方法<clinit>
的過程狞山。
<clinit>
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的叉寂。
特別注意的是萍启,靜態(tài)語句塊只能訪問到定義在它之前的類變量,定義在它之后的類變量只能賦值屏鳍,不能訪問勘纯。
static {
i = 0;
//靜態(tài)語句塊只能訪問到定義在它之前的類變量,定義在它之后的類變量只能賦值钓瞭,不能訪問驳遵。
//編譯報錯: Illegal forward reference
System.out.println(i);
}
static int i =1;
被動引用:不觸發(fā)初始化
注意:所有引用類的方式都不會觸發(fā)初始化稱為被動引用,下面是3個被動引用例子(對比上文"類何時初始化"進行理解):
- 通過子類引用父類靜態(tài)字段山涡,不會導致子類初始化超埋;
- 通過數(shù)組定義引用類搏讶,不會觸發(fā)此類的初始化
- 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類霍殴,因此不會觸發(fā)定義常量的類的初始化
public class SuperClass {
public static final String HELLO = "hello";
static {
System.out.println("super static class");
}
public static int value = 123;
public SuperClass(){
System.out.println("super class 初始化");
}
}
class SubClass extends SuperClass{
static {
System.out.println("sub static class");
}
public SubClass(){
System.out.println("sub class 初始化");
}
}
class Main {
/**
* 輸出結(jié)果:
* super static class
* 123
* hello
*/
public static void main(String[] args) {
// 被動引用1 :子類加載父類靜態(tài)字段媒惕,初始化父類,但不會導致子類初始化来庭;
System.out.println(SubClass.value);
// 被動引用2 : 通過數(shù)組定義引用類妒蔚,不會觸發(fā)此類的初始化
// 觸發(fā)的是一個虛擬機自動生成的,直接繼承于java.lang.Object的子類月弛,創(chuàng)建動作由字節(jié)碼指令newarray觸發(fā)肴盏。并沒有觸發(fā)真正的SuperClass的初始化
SuperClass[] sca = new SuperClass[10];
// 被動引用3 : 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類帽衙,因此不會觸發(fā)定義常量的類的初始化
System.out.println(SuperClass.HELLO);
}
}
類加載順序解析
分析如下代碼菜皂,控制臺輸出順序是什么?類變量count1厉萝,count2最終輸出為多少蚕钦?
public class Singleton {
private static Singleton singleton = new Singleton();
public static int count1;
public static int count2 = 5;
private Singleton() {
count1++;
count2++;
System.out.println("構(gòu)造函數(shù) count1 = " + count1 + " | count2 =" + count2);
}
// 對象一建立就運行構(gòu)造代碼塊了额湘,而且優(yōu)先于構(gòu)造函數(shù)執(zhí)行颈墅。有對象建立存哲,才會運行構(gòu)造代碼塊
{
System.out.println("構(gòu)造代碼塊 count1 = " + count1 + " | count2 =" + count2);
}
static {
System.out.println("靜態(tài)代碼塊 count1 = " + count1 + " | count2 =" + count2);
}
public static Singleton getInstance() {
return singleton;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(Singleton.count1);
System.out.println(Singleton.count2);
}
}
分析一下流程:
主方法中調(diào)用了類的靜態(tài)方法 getInstance(),會觸發(fā)類的初始化翩剪。前文介紹過類初始化即調(diào)用clinit
方法乳怎,該方法是編譯器自動按序收集合并類中的所有類變量的賦值動作和靜態(tài)語句塊。代碼中有三個類變量和一個靜態(tài)語句塊前弯,按順序合并生成clinit
方法蚪缀,最先執(zhí)行的為類變量singleton
的賦值動作。調(diào)用構(gòu)造函數(shù)恕出,同時構(gòu)造代碼塊在構(gòu)造函數(shù)之前询枚。此時count1 , count2 還未賦值,輸出結(jié)果為0剃根,++后變成1。再繼續(xù)執(zhí)行clinit
方法給count1前方,count2賦值狈醉,便覆蓋了之前的++操作。最終結(jié)果為1和5惠险。
最終結(jié)果如下苗傅。可以試著改變代碼順序班巩,自己分析不同的輸出結(jié)果渣慕。
構(gòu)造代碼塊 count1 = 0 | count2 =0
構(gòu)造函數(shù) count1 = 1 | count2 =1
靜態(tài)代碼塊 count1 = 1 | count2 =5
1
5