類(lèi)的生命周期一共分為七個(gè)階段:

類(lèi)加載過(guò)程為加載、驗(yàn)證、準(zhǔn)備、解析和初始化五個(gè)部分,其中驗(yàn)證、準(zhǔn)備和解析三個(gè)部分又被稱(chēng)為 連接(Linking)。
這些過(guò)程并不是嚴(yán)格的線性過(guò)程,中間會(huì)穿插執(zhí)行。比如加載為完成前,連接過(guò)程可能已經(jīng)開(kāi)始(比如文件格式的校驗(yàn));比如解析可能發(fā)生在初始化前也可能在初始化后等等。
現(xiàn)在對(duì)這 5 個(gè)階段做詳細(xì)分析。
1. 加載
把編譯后的 Class 文件載入內(nèi)存,創(chuàng)建一個(gè) java.lang.Class 對(duì)象。
這個(gè)階段要完成三件事情:
- 使用類(lèi)的 全限定名 來(lái)獲取類(lèi)的二進(jìn)制字節(jié)流。
- 將二進(jìn)制字節(jié)流的 靜態(tài)結(jié)構(gòu) 進(jìn)行轉(zhuǎn)化,存儲(chǔ)為方法區(qū)的 運(yùn)行時(shí)結(jié)構(gòu)。
- 在內(nèi)存中生成
java.lang.Class對(duì)象,作為訪問(wèn)的入口。經(jīng)常使用 Java 反射會(huì)對(duì)java.lang.Class對(duì)象非常熟悉,因?yàn)榭梢詮脑搶?duì)象中獲取到很多類(lèi)的信息,比如字段、方法等。
Class 文件的來(lái)源可以多種多樣,由不同的類(lèi)加載器實(shí)現(xiàn)不同來(lái)源加載,比如:
- 本地的 Class 文件直接加載。比如 classpath 路徑下的
.class文件。 - 網(wǎng)絡(luò)加載。
- 壓縮包中加載,zip、jar 或者 war 加載。比如第三方類(lèi)庫(kù)打包壓縮在 jar 中,部署到 tomcat 的 Class 文件打包在 war 中。
- 運(yùn)行時(shí)創(chuàng)建。Java 提供了一些工具類(lèi)來(lái)實(shí)現(xiàn),比如
java.lang.reflect包中的 Proxy 工具,可以直接在內(nèi)存中創(chuàng)建動(dòng)態(tài)代理對(duì)象。 - 由其他文件生成,比如 JSP 文件。

我們可以也自定義類(lèi)加載器來(lái)實(shí)現(xiàn)從其他渠道加載 Class 文件。
載入內(nèi)存后,生成 java.lang.Class 對(duì)象非常特別,因?yàn)樗淮娣旁诜椒▍^(qū)中而不是堆中。
2. 驗(yàn)證
驗(yàn)證是連接的第一步。對(duì) Class 文件的格式檢查,確保滿(mǎn)足 JVM 規(guī)范,避免產(chǎn)生虛擬機(jī)的安全問(wèn)題。
因?yàn)樯厦嬲f(shuō)到 Class 文件不一定是 javac 編譯產(chǎn)生的,有各種方式可以創(chuàng)建,對(duì)于編譯時(shí)做的數(shù)組越界、對(duì)象類(lèi)型轉(zhuǎn)換錯(cuò)誤等問(wèn)題,類(lèi)加載過(guò)程還需要再次校驗(yàn)。
不過(guò)驗(yàn)證還是挺耗性能的,如果 Class 文件已經(jīng)被反復(fù)驗(yàn)證多次,可以使用 -Xverify:none 來(lái)縮短類(lèi)加載時(shí)間。
驗(yàn)證主要有四種驗(yàn)證:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
2.1. 文件格式驗(yàn)證
文件格式驗(yàn)證,確保符合 Class 文件規(guī)范,能被當(dāng)前版本的虛擬機(jī)處理。比如:
檢查魔數(shù)(Magic Number)是否是 Class 文件(0xCAFEBABY)。
檢查主次版本對(duì)不對(duì)。
-
檢查常量池的類(lèi)型和索引是否正確。
…...
在經(jīng)歷過(guò)這個(gè)階段后,字節(jié)流才會(huì)被存儲(chǔ)到方法區(qū)中。所以文件校驗(yàn)發(fā)生在 加載 階段還未結(jié)束前。經(jīng)過(guò)文件格式校驗(yàn)后,后續(xù)的校驗(yàn)都是基于方法區(qū)執(zhí)行的。
2.2. 元數(shù)據(jù)驗(yàn)證
元數(shù)據(jù)驗(yàn)證,字節(jié)碼語(yǔ)義分析,看是否符合語(yǔ)法規(guī)范。比如:
是否有父類(lèi)。除了
java.lang.Object,所有的類(lèi)都要有父類(lèi)。是否繼承了不允許被繼承的類(lèi)。比如被
final修飾類(lèi)不允許被繼承。如果不是抽象類(lèi),是否實(shí)現(xiàn)了父類(lèi)或接口要求實(shí)現(xiàn)的方法。
-
是否類(lèi)的字段和方法和父類(lèi)有矛盾。比如覆蓋了父類(lèi)的
final字段。…...
這些語(yǔ)法規(guī)范如果是編譯的 Class 文件,在編譯期間也會(huì)做校驗(yàn)。但因?yàn)?Class 文件來(lái)源多樣,并不能確保遵循語(yǔ)法規(guī)范,所以這里還要再進(jìn)行一次驗(yàn)證。
2.3. 字節(jié)碼驗(yàn)證
字節(jié)碼校驗(yàn)。對(duì)字節(jié)碼 數(shù)據(jù)流 和 控制流 進(jìn)行分析,確定語(yǔ)義正確。這個(gè)是所有校驗(yàn)最復(fù)雜的階段。比如:
正確的操作指令。操作操作局部變量表和操作數(shù)棧的數(shù)據(jù),指令類(lèi)型要正確。
正確的跳轉(zhuǎn)指令。跳轉(zhuǎn)到正確的字節(jié)碼。
-
正確的類(lèi)型轉(zhuǎn)換。比如子類(lèi)可以賦值給父類(lèi),但父類(lèi)不允許賦值子類(lèi)。
…...
這里舉個(gè)校驗(yàn)的例子。
在執(zhí)行字節(jié)碼執(zhí)行的時(shí)候,對(duì)局部變量表和操作數(shù)棧的數(shù)據(jù)進(jìn)行操作時(shí),需要使用正確的指令類(lèi)型。比如局部變量表索引 1 是 int 類(lèi)型,加載它需要用 iload_1 字節(jié)碼,而不是 lload_1 、fload_1 和 dload_1 。在元數(shù)據(jù)校驗(yàn)階段就可以做強(qiáng)制約束,排除錯(cuò)誤使用。
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1 # 因?yàn)榫植孔兞款?lèi)型為 int,使用 iload 加載
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 32: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Loblee/demo/jvm/stack/SimpleObject;
0 4 1 a I # 局部變量類(lèi)型為 int
0 4 2 b I # 局部變量類(lèi)型為 int
經(jīng)過(guò)了復(fù)雜的字節(jié)碼驗(yàn)證,其實(shí)還是無(wú)法確保運(yùn)行的安全。屬于著名的 "Halting Problem"。
復(fù)雜的驗(yàn)證也十分消耗性能,HotSpot 虛擬機(jī)做了很多優(yōu)化。
2.4. 符號(hào)引用驗(yàn)證
符號(hào)引用校驗(yàn)。這個(gè)發(fā)生在符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候,屬于 解析 階段的驗(yàn)證,用來(lái)確認(rèn)引用一定會(huì)被訪問(wèn)到。比如:
符號(hào)引用的全限定名是否能找到對(duì)應(yīng)的類(lèi)。
符號(hào)引用的字段描述符或方法描述符是否能在類(lèi)中找到對(duì)應(yīng)字段和方法。
-
符號(hào)引用的類(lèi)、字段和方法的作用域(public、protected、private、default)是否可以被當(dāng)前類(lèi)訪問(wèn)。
…...
符號(hào)引用驗(yàn)證也說(shuō)明了這幾個(gè)階段不是線性的,而是會(huì)會(huì)穿插執(zhí)行。
無(wú)法通過(guò)符號(hào)引用驗(yàn)證的話,會(huì)拋出相關(guān)的異常,比如 java.lang.IllegalAccessError 、 java.lang.NoSuchFieldError 、java.lang.NoSuchMethodError 等等。
3. 準(zhǔn)備
為 類(lèi)變量 分配內(nèi)存,設(shè)置初始值。
分配的初始值具體是多少?各個(gè)數(shù)據(jù)的默認(rèn)零值,有 0、0L、null、false 等。
比如下面的類(lèi)變量 a:
public class SimpleObject {
public static String a = "hello world";
public static String b = 2;
}
準(zhǔn)備 階段給 a 的初始值是 null,而不是 “hello world” 字符串,給 b 的初始值是 0 而不是 2。a 和 b 的真實(shí)賦值發(fā)生在 初始化 階段,放在了類(lèi)構(gòu)造器 <clinit>() 中,使用 putstatic 指令實(shí)現(xiàn)。
上面是類(lèi)變量的處理過(guò)程,常量并非如此。如果是字符串或者基礎(chǔ)數(shù)據(jù)類(lèi)型(int、long、float 等等)的常量類(lèi)型,準(zhǔn)備階段會(huì)直接分配具體數(shù)據(jù)。
還是 SimpleObject 對(duì)象,我們使用 final 關(guān)鍵字將 a 和 b 修飾為類(lèi)常量:
public class SimpleObject {
public static final String a = "hello world";
public static final String b = 2;
}
準(zhǔn)備階段,會(huì)直接從運(yùn)行時(shí)常量池中取出 "hello world" 字符串給 a 賦值,從常量池中取出 2 對(duì) b 賦值(這時(shí)候 2 被加入到常量池中)。
注意,準(zhǔn)備階段只是處理 類(lèi)變量,而不是 實(shí)例變量。實(shí)例變量的在類(lèi)實(shí)例化后一同和實(shí)例對(duì)象分配在堆中。
4. 解析
解析的過(guò)程,就是將符號(hào)引用轉(zhuǎn)為直接引用的過(guò)程。
符號(hào)引用中主要有:
- 類(lèi)和接口的全限定名。
- 字段名稱(chēng)和描述符。
- 方法名稱(chēng)和描述符。
對(duì)應(yīng)在 class 文件中的常量類(lèi)型為 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 。編譯后在 class 文件的靜態(tài)常量池中,類(lèi)加載后進(jìn)入運(yùn)行時(shí)常量池。之所以需要符號(hào)引用,是因?yàn)?javac 將代碼編譯成 class 文件時(shí),是不知道這些類(lèi)、字段和方法在內(nèi)存中的位置的,需要有一個(gè)符號(hào)來(lái)代替。
比如我們有一個(gè)類(lèi) ObjectA ,引用了 ObjectB 作為成員變量:
public class ObjectA {
private ObjectB b;
public void setB(ObjectB b) {
this.b = b;
}
public ObjectB getB() {
return b;
}
}
使用 javap 查看這個(gè)類(lèi)的符號(hào)引用:

直接引用主要有:
- 目標(biāo)指針,比如 Class 對(duì)象、類(lèi)變量、類(lèi)方法在方法區(qū)的指針。
- 相對(duì)偏移量,比如實(shí)例變量在堆中的相對(duì)偏移量。
- 能間接定位到目標(biāo)的 句柄。
可以直接引用對(duì)應(yīng)的目標(biāo)已經(jīng)在內(nèi)存中了。直接引用和虛擬機(jī)的內(nèi)存布局相關(guān),在不同虛擬機(jī)的直接引用是不一樣的。
這些在未解析前都是字符串,存在方法區(qū)的運(yùn)行時(shí)常量池里(HotSpot 1.8 方法區(qū)實(shí)現(xiàn)做了調(diào)整,符號(hào)引用被放到了由本地內(nèi)存組成的元空間中),字節(jié)碼解釋器在執(zhí)行字節(jié)碼的時(shí)候是無(wú)法直接使用符號(hào)引用的,所以需要翻譯成直接引用,可以是內(nèi)存中的
這個(gè)解析過(guò)程不一定在 初始化 前執(zhí)行,也有可能延遲到一個(gè)符號(hào)引用需要使用到的時(shí)候,在棧幀進(jìn)行動(dòng)態(tài)鏈接。

初始化前的解析的被稱(chēng)為 靜態(tài)綁定 ,棧幀里的解析被稱(chēng)為 動(dòng)態(tài)綁定。
解析完成后,符號(hào)引用對(duì)應(yīng)的直接引用會(huì)被記錄在運(yùn)行時(shí)常量池中,后續(xù)如果重復(fù)解析,直接返回對(duì)應(yīng)的直接引用。
5. 初始化
經(jīng)過(guò)了加載和連接(校驗(yàn)、準(zhǔn)備和解析),類(lèi)已經(jīng)載入內(nèi)存并且分配了初始內(nèi)存,開(kāi)始進(jìn)行初始化。
加載過(guò)程我們可以自定義類(lèi)加載器(比如 ClassLoader) 來(lái)接管加載,連接完全是虛擬機(jī)自動(dòng)處理的,而初始化才開(kāi)始正式執(zhí)行字節(jié)碼。
初始化就是執(zhí)行類(lèi)構(gòu)造器 <clinit>() 的過(guò)程。<clinit>() 由類(lèi)變量賦值加上 static{} 語(yǔ)句塊的代碼合并而成,這個(gè)并不是必須的,如果沒(méi)有靜態(tài)變量賦值過(guò)程的話,是不會(huì)生成 <clinit>() 的。
在 <clinit()> 調(diào)用一些指令來(lái)實(shí)現(xiàn)靜態(tài)變量的賦值操作, 比如使用 putstatic 。
準(zhǔn)備類(lèi) SimpleObject 如下:
public class SimpleObject {
public static String a = "hello world";
public static int b = 100;
public static ObjectC c = new ObjectC();
public static Class d = ObjectD.class;
public static int e = ObjectE.e;
public static int f = ObjectF.getF();
public static int h = 10;
public static String i = ObjectI.I;
public static int k = ObjectJ.k;
static {
ObjectH.h = h;
}
}
使用 javap 查看 SimpleObject 的類(lèi)構(gòu)造器 <clinit>:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc #2 // String hello world
2: putstatic #3 // Field a:Ljava/lang/String;
5: bipush 100
7: putstatic #4 // Field b:I
10: new #5 // class oblee/demo/jvm/pre/ObjectC
13: dup
14: invokespecial #6 // Method oblee/demo/jvm/pre/ObjectC."<init>":()V
17: putstatic #7 // Field c:Loblee/demo/jvm/pre/ObjectC;
20: ldc #8 // class oblee/demo/jvm/pre/ObjectD
22: putstatic #9 // Field d:Ljava/lang/Class;
25: getstatic #10 // Field oblee/demo/jvm/pre/ObjectE.e:I
28: putstatic #11 // Field e:I
31: invokestatic #12 // Method oblee/demo/jvm/pre/ObjectF.getF:()I
34: putstatic #13 // Field f:I
37: bipush 10
39: putstatic #14 // Field h:I
42: ldc #16 // String objectI
44: putstatic #17 // Field i:Ljava/lang/String;
47: getstatic #18 // Field oblee/demo/jvm/pre/ObjectJ.k:I
50: putstatic #19 // Field k:I
53: getstatic #14 // Field h:I
56: putstatic #20 // Field oblee/demo/jvm/pre/ObjectH.h:I
59: return
如果靜態(tài)變量是 字符串類(lèi)型,作為字面量加入了常量池,初始化時(shí)從常量池中取出賦值。比如上面的 "hello world" 直接從常量池取出賦值給 a。
如果靜態(tài)變量是 基礎(chǔ)數(shù)據(jù)類(lèi)型,直接使用指令載入。比如上面的 bipush 指令。
如果靜態(tài)變量是個(gè) 類(lèi)實(shí)例的引用類(lèi)型,但是類(lèi)還未加載,會(huì)再次觸發(fā)這個(gè)類(lèi)的類(lèi)加載流程。
初始化不是在類(lèi)加載后就馬上執(zhí)行的,有一定的觸發(fā)條件。JVM 規(guī)范沒(méi)有規(guī)定什么時(shí)候開(kāi)始進(jìn)行類(lèi)加載流程,但對(duì)類(lèi)初始化有進(jìn)行嚴(yán)格的規(guī)定,有四條字節(jié)碼指令 new 、 getstatic 、putstatic 、invokestatic 這 4 條指令,如果沒(méi)有初始化會(huì)先觸發(fā)初始化。
常見(jiàn)的場(chǎng)景有:
-
使用 new 關(guān)鍵詞實(shí)例化對(duì)象。上面對(duì)靜態(tài)變量 c 賦值,需要對(duì)類(lèi)
ObjectC進(jìn)行實(shí)例化,所以會(huì)觸發(fā)ObjectC的初始化。 -
讀取類(lèi)的靜態(tài)變量。上面賦值靜態(tài)變量 e,
getstatic #10,觸發(fā)ObjectE的初始化。 -
設(shè)置類(lèi)的靜態(tài)變量。上面設(shè)置
ObjectH的靜態(tài)變量 h,putstatic #20,觸發(fā)ObjectH的初始化。 -
調(diào)用類(lèi)的靜態(tài)方法。上面調(diào)用
ObjectF的靜態(tài)方法getF(),invokestatic #12,觸發(fā)ObjectF的初始化。
比如下面幾種情況不會(huì)執(zhí)行類(lèi)初始化:
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,只觸發(fā)父類(lèi)的初始化,不會(huì)觸發(fā)子類(lèi)的初始化。
- 使用對(duì)象數(shù)組,不會(huì)觸發(fā)該類(lèi)初始化。
- 常量被存入常量池中,沒(méi)有直接引用常量的類(lèi),不會(huì)觸發(fā)定義常量的類(lèi)的初始化。
- 通過(guò)類(lèi)名獲取 Class 對(duì)象,不會(huì)觸發(fā)初始化。比如上面使用
ObjectD.class并不會(huì)觸發(fā)ObjectD類(lèi)的初始化。 - 使用
Class.forName()加載類(lèi),參數(shù) initialize 為 false 不會(huì)觸發(fā)初始化。 - 通過(guò) ClassLoader 默認(rèn)的
loadClass方法加載類(lèi)不會(huì)觸發(fā)初始化。