虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存灵巧,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化玻褪,最終形成可以被虛擬機(jī)直接使用的Java類型许师,這就是虛擬機(jī)的類加載機(jī)制房蝉。
1.類的生命周期
從圖中可以看到,類的生命周期共有7個階段:加載->驗(yàn)證->準(zhǔn)備->解析->初始化->使用->卸載微渠,其中驗(yàn)證搭幻、準(zhǔn)備、解析這3個階段合起來又稱為連接階段逞盆。
虛擬機(jī)規(guī)范定義了以下4種情況必須立即對類進(jìn)行初始化:
- 遇到new粗卜、getstatic、putstatic纳击、invokestatic這4條字節(jié)碼指令時续扔,如果類還沒有初始化過則先對其進(jìn)行初始化。與這幾條指令對應(yīng)的場景通常有:采用new關(guān)鍵字實(shí)例化一個類的對象焕数、讀取或設(shè)置類的靜態(tài)字段(被final關(guān)鍵字修飾并且在編譯期已經(jīng)生成常量的除外)纱昧、調(diào)用類的靜態(tài)方法;
- 對類進(jìn)行反射調(diào)用時堡赔,如果該類還沒有初始化识脆,則先觸發(fā)其初始化;
- 當(dāng)對類進(jìn)行初始化時,如果發(fā)現(xiàn)其父類還沒有初始化過灼捂,則先觸發(fā)其父類的初始化离例;
- 當(dāng)虛擬機(jī)啟動時,會先對入口類(包含main()方法的類)進(jìn)行初始化悉稠;
以上這4種引用方式都稱為對類的主動引用宫蛆,有且只有這4種引用方式會觸發(fā)類的初始化,其他引用方式都是類的被動引用方式的猛,均不會觸發(fā)類的初始化耀盗,例如以下幾種被動引用方式:
- 通過子類調(diào)用父類的靜態(tài)變量、靜態(tài)方法不會觸發(fā)子類初始化
public class SuperClass {
public static String name = "super";
static {
System.out.println("super class init...");
}
public static void callSuper() {
System.out.println("method called in super class...");
}
}
public class ChildClass extends SuperClass{
static {
System.out.println("child class init...");
}
}
測試代碼:
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("print super class name: " + ChildClass.name);
ChildClass.callSuper();
}
測試結(jié)果如下:
super class init...
print super class name: super
method called in super class...
這里可以看到卦尊,子類引用了父類的靜態(tài)變量叛拷,通過子類調(diào)用了父類的靜態(tài)方法,但是只觸發(fā)了父類的初始化岂却,并沒有觸發(fā)子類的初始化忿薇。對于靜態(tài)字段以及靜態(tài)方法調(diào)用,只有直接定義這個字段和方法的類才會被初始化躏哩。
- 對類里的常量字段進(jìn)行引用不會觸發(fā)類的初始化
public class ConstClass {
public static int a = 10;
static {
System.out.println("ConstClass init...");
}
}
public class ConstClass2 {
public static final int a = 10;
static {
System.out.println("ConstClass2 init...");
}
}
測試代碼:
public static void main(String[] args) throws Throwable {
System.out.println(ConstClass.a);
System.out.println(ConstClass2.a);
}
測試結(jié)果如下:
ConstClass init...
10
10
從以上結(jié)果可以看到署浩,ConstClass有觸發(fā)類的初始化,ConstClass2并沒有觸發(fā)類的初始化震庭,這是因?yàn)镃onstClass2的字段是static final修飾的瑰抵,在編譯的時候該字段值已放到常量池里了你雌,對該字段的引用僅僅是對常量池里該常量的引用器联。
- 通過數(shù)組定義來引用類不會觸發(fā)類的初始化
public static void main(String[] args) throws Throwable {
SuperClass[] arr = new SuperClass[10];
}
定義了一個一維數(shù)組,這樣并不會觸發(fā)類的初始化婿崭。
2.加載
- 通過類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流拨拓;
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu);
- 在Java堆中生成一個代表這個類的java.lang.Class對象氓栈,作為方法區(qū)這些數(shù)據(jù)的訪問入口渣磷;
3.驗(yàn)證
驗(yàn)證階段主要是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全授瘦。大致分為4個階段:文件格式驗(yàn)證醋界、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證提完、符號引用驗(yàn)證形纺。
4.準(zhǔn)備
準(zhǔn)備階段主要是為類變量分配內(nèi)存并設(shè)置初始值,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配徒欣。 類變量一般指static修飾的變量逐样,通常情況下初始值指的是數(shù)據(jù)類型的零值。例如:
public static int a = 10;
對于以上代碼,準(zhǔn)備階段會將變量a的值設(shè)置為初始值0而不是10脂新,設(shè)置成10是在類的初始化階段里執(zhí)行類的構(gòu)造方法<clinit>()時發(fā)生的挪捕。
public static final int a = 10;
相比之前對字段a的定義這里多了final修飾,在編譯時生成的該字段信息的屬性表中會有一個ConstantValue屬性争便,該ConstantValue的屬性值為字面量10级零,這樣在準(zhǔn)備階段會將字段a的初始值直接設(shè)置為10。
5.解析
解析是將常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用的過程始花,主要包括4種解析過程:類或接口的解析妄讯、字段解析、類方法解析酷宵、接口方法解析亥贸。
- 符號引用:以一組符號來描述所引用的目標(biāo),它與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān)浇垦,引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中炕置;
- 直接引用:它可以是直接指向目標(biāo)的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄男韧,直接引用是與虛擬實(shí)現(xiàn)的內(nèi)存布局相關(guān)的朴摊,直接引用的目標(biāo)是一定存在于內(nèi)存中的。
兩者的區(qū)別可以簡單理解為此虑,直接引用就是對目標(biāo)對象在內(nèi)存中地址的引用甚纲,符號引用描述的只是一個字面上的邏輯關(guān)系,并不一定會反映到具體的內(nèi)存上朦前。
6.初始化
類初始化是比較關(guān)鍵的一步介杆,前面幾個步驟都是由虛擬機(jī)來主導(dǎo)執(zhí)行的,它具體是怎么執(zhí)行的我們并不清楚韭寸,到這一步才真正開始與我們寫的java代碼相關(guān)聯(lián)起來春哨。
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
- <clinit>()方法是由編譯器自動收集類中的類變量賦值動作和靜態(tài)語句塊(static {}塊)中的語句合并產(chǎn)生的恩伺;
- 虛擬機(jī)會保證子類的<clinit>()方法執(zhí)行之前赴背,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。由于java.lang.Object是所有類的父類晶渠,所以肯定是java.lang.Object類的<clinit>()方法第一個執(zhí)行凰荚;
- <clinit>()方法對于類和接口來說并不是必須的,例如類中沒有靜態(tài)語句塊褒脯,也沒有靜態(tài)變量便瑟,這樣并不一定會生成<clinit>()方法;
- 虛擬機(jī)在執(zhí)行類的<clinit>()方法時會進(jìn)行加鎖同步憨颠,由虛擬機(jī)來保證<clinit>()方法只會執(zhí)行一次胳徽;
以下情況不會生成<clinit>()方法:
1.類中沒有任何類變量(staitc修飾的字段)以及靜態(tài)語句塊积锅;
2.類中只有static final修飾的字段,這只會在常量池中生成一個常量养盗;
3.類中雖然有類變量缚陷,但是并沒有賦值動作;
public class TestInitClass {
public static final int A = 0;
public static int B;
}
以上代碼編譯時并不會生成<clinit>()方法往核,字段“A”是static final修飾的箫爷,會在編譯時生成一個ConstantValue常量,字段“B”雖然不會生成常量聂儒,但是代碼里并沒有對其進(jìn)行賦值虎锚,所以編譯時并不會生成<clinit>()方法。
典型的會生成<clinit>()方法的代碼如下:
public class TestInitClass {
public static int A = 0;
static {
System.out.println("");
}
}
7.接口的初始化
看到相關(guān)資料說衩婚,接口類在編譯時也會生成<clinit>()方法窜护。首先關(guān)于接口類有2點(diǎn)必須先清楚:1.接口類里不能定義靜態(tài)語句塊;2.接口類里的字段會被自動轉(zhuǎn)為public static final類型的非春,不管代碼里有沒有為變量定義public static final這幾個關(guān)鍵字柱徙。鑒于這2點(diǎn),本人對接口類也會生成<clinit>()方法存疑奇昙,前面說明過這種情況下并不會生成<clinit>()方法护侮,本著懷疑的態(tài)度自己編寫一個測試類來看看:
public interface TestInterface {
int b = 10;
void test();
}
對該接口采用javac命令編譯后再用javap -v TestInterface.class命令查看字節(jié)碼信息:
public interface TestInterface
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #11 // TestInterface
#2 = Class #12 // java/lang/Object
#3 = Utf8 b
#4 = Utf8 I
#5 = Utf8 ConstantValue
#6 = Integer 10
#7 = Utf8 test
#8 = Utf8 ()V
#9 = Utf8 SourceFile
#10 = Utf8 TestInterface.java
#11 = Utf8 TestInterface
#12 = Utf8 java/lang/Object
{
public static final int b;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public abstract void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "TestInterface.java"
從上面可以看出:
1.雖然代碼里我們定義的是“int b”,但是編譯后的字節(jié)碼里“b”的實(shí)際類型是“public static final int b”储耐;
2.這里并沒有<clinit>()方法生成羊初;
這僅僅是個人的見解,并不一定正確什湘,歡迎批評指正长赞。
java類加載機(jī)制系列文章: