序言
今天我們聊一聊Java中類(lèi)加載機(jī)制,簡(jiǎn)單來(lái)說(shuō),就是程序運(yùn)行過(guò)程中,虛擬機(jī)把類(lèi)加載到內(nèi)存中,可以給程序使用。
我們先介紹原理,掌握原理之后,我們就應(yīng)用這個(gè)原理,來(lái)分析兩個(gè)具體的例子。
類(lèi)文件基本結(jié)構(gòu)
我們都知道,程序中的java文件,編譯之后,會(huì)生成對(duì)應(yīng)的Class文件,Class文件是一組以8字節(jié)為單位的二進(jìn)制流,各數(shù)據(jù)項(xiàng)按照嚴(yán)格的順序緊密排列,中間沒(méi)有任何間隔。
這么說(shuō)可能有點(diǎn)抽象,我們舉個(gè)例子:
public class Test {
public int add(int a, int b) {
return a + b;
}
}
經(jīng)過(guò)編譯之后得到Test.class文件,然后我們執(zhí)行下面命令:
hexdump Test.class:

上圖是用十六進(jìn)制來(lái)表示Test.class二進(jìn)制流。每一位排列緊密,都有其含義。
下面,我們介紹一下Class文件中具體包含哪些內(nèi)容:
-
魔數(shù):1-4字節(jié),用來(lái)確定這個(gè)文件是否JVM認(rèn)可的Class文件,它的值位:0xCAFEBABE。 -
版本號(hào):5-6字節(jié)標(biāo)識(shí)次版本號(hào)(minor version),7-8字節(jié)表示主版本號(hào)(major version)。 -
常量池:常量池中常量的數(shù)量不是固定的,主要存放字面量(文本字符串、final常量)和符號(hào)引用(類(lèi)和接口全限定名、字段名稱(chēng)與描述、方法名稱(chēng)與發(fā)描述)。 -
訪(fǎng)問(wèn)標(biāo)志:主要用來(lái)識(shí)別類(lèi)和接口的訪(fǎng)問(wèn)信息(是否為public、是否為final、是否為接口、是否為抽象、是否為注解、是否為枚舉等)。 -
類(lèi)、父類(lèi)、接口索引:用來(lái)確定該類(lèi)和父類(lèi)的全限定名,以及描述該類(lèi)實(shí)現(xiàn)了哪些接口。 -
字段表集合:描述接口或者類(lèi)中聲明的變量、字段。包括類(lèi)變量和實(shí)例變量,不包括方法內(nèi)的局部變量。 -
方法表集合:用來(lái)描述方法相關(guān)信息
知道了Class存儲(chǔ)格式細(xì)節(jié),那么類(lèi)是如何加載到JVM中的呢?不急,下面我們會(huì)介紹。
擴(kuò)展:結(jié)合上一篇文章JVM內(nèi)存結(jié)構(gòu),我們就不難知道,類(lèi)結(jié)構(gòu)被加載到JVM后存儲(chǔ)在JVM方法區(qū)中。
類(lèi)加載流程
類(lèi)的加載是虛擬機(jī)通過(guò)類(lèi)的全限定名來(lái)獲取此類(lèi)的二進(jìn)制字節(jié)流。
我們先看一下,類(lèi)加載流程圖:
[圖片上傳失敗...(image-c9e3f4-1548600350447)]
由圖可知,類(lèi)加載流程有七個(gè)步驟,分別是:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載。下面依次介紹這幾個(gè)步驟:
-
加載:類(lèi)加載的第一個(gè)階段,JVM將字節(jié)碼從各個(gè)位置(網(wǎng)絡(luò)、磁盤(pán))轉(zhuǎn)換為二進(jìn)制字節(jié)流加載到內(nèi)存中,接著在JVM的方法區(qū)為這個(gè)類(lèi)創(chuàng)建一個(gè)Class對(duì)象,這個(gè)Class對(duì)象是該類(lèi)各種數(shù)據(jù)的訪(fǎng)問(wèn)入口。 -
驗(yàn)證:類(lèi)加載完之后,JVM會(huì)對(duì)二進(jìn)制字節(jié)流進(jìn)行校驗(yàn),只有符合規(guī)范的文件才能被正確執(zhí)行。校驗(yàn)包括JVM規(guī)范校驗(yàn)(如文件是否已0xcafebabe開(kāi)頭,主次版本號(hào)是否在JVM處理范圍內(nèi)等)和代碼邏輯校驗(yàn)(主要是針對(duì)代碼的數(shù)據(jù)流和控制流,確保運(yùn)行該字節(jié)碼不會(huì)出現(xiàn)致命錯(cuò)誤。例如方法接受int型參數(shù),卻傳一個(gè)String類(lèi)型)。 -
準(zhǔn)備:這個(gè)階段,JVM為類(lèi)變量(static修飾,區(qū)別成員變量)分配內(nèi)存并初始化,這里的初始化是指為類(lèi)變量賦予Java中該數(shù)據(jù)類(lèi)型的默認(rèn)值(如String的默認(rèn)值是null,int默認(rèn)值是0),但如果類(lèi)變量被final修飾,則會(huì)直接賦予用戶(hù)代碼中的初始值(這點(diǎn)也比較好理解,final修飾的變量一旦賦值就不能修改,所以只能一開(kāi)始就直接賦予用戶(hù)想要的值)。 -
解析:準(zhǔn)備階段過(guò)后,JVM針對(duì)類(lèi)和接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄、調(diào)用點(diǎn)限定符7類(lèi)引用進(jìn)行解析。主要任務(wù)是把它在常量池中的符號(hào)引用替換為其在內(nèi)存中的直接引用。(這個(gè)階段堆我們來(lái)說(shuō)幾乎透明,了解一下就行了) -
初始化:用戶(hù)編寫(xiě)的Java代碼從這個(gè)階段才開(kāi)始執(zhí)行。在這個(gè)階段,JVM會(huì)根據(jù)語(yǔ)句執(zhí)行順序?qū)︻?lèi)對(duì)象進(jìn)行初始化,一般來(lái)說(shuō),JVM遇到5種情況會(huì)觸發(fā)初始化。- 遇到new、getstataic、putstatic、invokestatic字節(jié)碼指令時(shí),如果類(lèi)沒(méi)有初始化,就會(huì)觸發(fā)其進(jìn)行初始化。場(chǎng)景包括通過(guò)new實(shí)例化對(duì)象、讀取或設(shè)置類(lèi)的靜態(tài)字段(final修飾除外)、調(diào)用類(lèi)的靜態(tài)方法。
- 對(duì)類(lèi)進(jìn)行反射調(diào)用時(shí),如果類(lèi)沒(méi)有初始化,則會(huì)觸發(fā)其初始化。
- 初始化一個(gè)類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還未初始化,則會(huì)先觸發(fā)其父類(lèi)初始化。
- 虛擬機(jī)啟動(dòng)時(shí),用戶(hù)指定一個(gè)執(zhí)行的主類(lèi)(包含main()的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。
- 使用動(dòng)態(tài)語(yǔ)言支持時(shí)(jdk1.7開(kāi)始),如果一個(gè)MethodHandle實(shí)例最后解析結(jié)果REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)未初始化,則先觸發(fā)其初始化。
-
使用:JVM從入口方法開(kāi)始執(zhí)行用戶(hù)代碼。(了解一下就行) -
卸載:用戶(hù)程序執(zhí)行完畢之后,JVM開(kāi)始銷(xiāo)毀創(chuàng)建的Class對(duì)象。最后負(fù)責(zé)運(yùn)行的JVM也退出內(nèi)存。(了解一下就行)
類(lèi)加載器
類(lèi)加載器是用來(lái)執(zhí)行類(lèi)加載動(dòng)作的角色。
類(lèi)和類(lèi)加載器息息相關(guān),判斷兩個(gè)類(lèi)是否相等,只有在這兩個(gè)類(lèi)被同一個(gè)類(lèi)加載器加載的情況下才有意義,否則即使是同一個(gè)類(lèi),被不同的類(lèi)加載器加載,他們也不是相等的。
類(lèi)加載器可以分為三類(lèi):
-
啟動(dòng)類(lèi)加載器:負(fù)責(zé)加載JAVA_HOME\lib目錄下的類(lèi)庫(kù)到內(nèi)存中。 -
擴(kuò)展類(lèi)加載器:負(fù)責(zé)加載JAVA_HOME\lib\ext目錄下的類(lèi)庫(kù)到內(nèi)存中 -
應(yīng)用類(lèi)加載器:負(fù)責(zé)加載用戶(hù)類(lèi)路徑上的類(lèi)庫(kù),如果應(yīng)用程序沒(méi)有實(shí)現(xiàn)自己的類(lèi)加載器,默認(rèn)使用這個(gè)類(lèi)加載器去加載程序中的類(lèi)庫(kù)。
既然有這么多加載器,那么加載類(lèi)的時(shí)候會(huì)選擇什么類(lèi)加載器呢?
著這個(gè)時(shí)候,需要提到類(lèi)加載器的雙親委派模型,流程圖如下:

如果一個(gè)類(lèi)加載器收到加載類(lèi)的請(qǐng)求,它不會(huì)立刻去加載,它會(huì)先請(qǐng)求父類(lèi)加載器,每個(gè)層次的類(lèi)加載器都是如此。層層傳遞,知道最高層的類(lèi)加載器,只有當(dāng)父類(lèi)加載器反饋?zhàn)约簾o(wú)法加載這個(gè)類(lèi),才會(huì)由當(dāng)前子類(lèi)加載器去加載該類(lèi)。
為什么要這么做呢?這是為了讓越基礎(chǔ)的類(lèi)由越高層的類(lèi)加載器去加載,如Object類(lèi),最后都會(huì)傳遞給最高層類(lèi)加載器去加載。類(lèi)的相等性,是由類(lèi)與類(lèi)加載器共同決定,這樣無(wú)論在何種類(lèi)加載器環(huán)境下都是同一個(gè)類(lèi)。相反,如果沒(méi)有雙親委派模型,每個(gè)類(lèi)加載器都會(huì)去加載Object,系統(tǒng)中就會(huì)出現(xiàn)多個(gè)不同Object類(lèi),如此一來(lái)系統(tǒng)最基礎(chǔ)的行為都無(wú)法保證了。
舉例分析原理
為了鞏固上面的類(lèi)加載原理,下面給出兩個(gè)例子,供大家分析。
例子一
public class Book {
public static void main(String[] args) {
staticMethod();
}
static Book book = new Book();
public Book() {
System.out.println("Book構(gòu)造方法");
System.out.println("Book的price="+price+",amount="+amount);
}
{
System.out.println("Book中普通代碼塊");
}
int price = 110;
static {
System.out.println("Book中靜態(tài)代碼塊");
}
public static void staticMethod() {
System.out.println("Book中靜態(tài)方法");
System.out.println("Book amount=" + amount);
}
static int amount = 112;
}
給各位同學(xué)5分鐘,寫(xiě)出這個(gè)程序輸出的內(nèi)容。
1.,,2,,,3,,,,4,,,,,5
各位同學(xué)有答案了嗎,正確的答案如下:
Book中普通代碼塊
Book構(gòu)造方法
Book的price=110,amount=0
Book中靜態(tài)代碼塊
Book中靜態(tài)方法
Book amount=112
如果你的答案跟這個(gè)一樣,恭喜你,你對(duì)類(lèi)加載機(jī)制已經(jīng)有了深刻的認(rèn)識(shí)。
下面,我解釋一下,答案為什么是這樣的:
- JVM在準(zhǔn)備階段,會(huì)為類(lèi)變量分配內(nèi)存并且初始化類(lèi)變量。此時(shí),變量book初始化為null,變量amount初始化為0。
- 進(jìn)入初始化階段后,main方法是程序的入口,所以Book是我們指定的主類(lèi),這時(shí)候JVM會(huì)初始化Book,即執(zhí)行類(lèi)構(gòu)造器。
- JVM對(duì)Book進(jìn)行初始化,首先是執(zhí)行類(lèi)構(gòu)造器(按順序收集類(lèi)中的靜態(tài)代碼塊和靜態(tài)變量賦值語(yǔ)句就組成類(lèi)構(gòu)造器),后執(zhí)行對(duì)象構(gòu)造器(按順序收集成員變量賦值語(yǔ)句和普通代碼塊,最后收集對(duì)象構(gòu)造器,組成對(duì)象構(gòu)造器)。
對(duì)于Book類(lèi),其類(lèi)構(gòu)造器可以表示為:
static Book book = new Book();
static {
System.out.println("Book中靜態(tài)代碼塊");
}
static int amount = 112;
首先執(zhí)行static Book book = new Book();,這條語(yǔ)句會(huì)觸發(fā)類(lèi)的實(shí)例化,于是JVM執(zhí)行對(duì)象構(gòu)造器,對(duì)象構(gòu)造器可以表示為:
{
System.out.println("Book中普通代碼塊");
}
int price = 110;
public Book() {
System.out.println("Book構(gòu)造方法");
System.out.println("Book的price="+price+",amount="+amount);
}
首先,輸出Book中普通代碼塊,然后給price賦值為110,接著執(zhí)行對(duì)象構(gòu)造方法,先輸出Book構(gòu)造方法,再輸出Book的price=110,amount=0(靜態(tài)變量amount再準(zhǔn)備階段賦值為0)。
Book對(duì)象構(gòu)造器執(zhí)行完畢之后,繼續(xù)執(zhí)行靜態(tài)代碼塊,輸出Book中靜態(tài)代碼塊,然后給靜態(tài)變量賦值為112,這時(shí)類(lèi)構(gòu)造器也執(zhí)行完畢。
回到入口方法main中,執(zhí)行staticMethod方法,先輸出Book中靜態(tài)方法,在輸出Book amount=112,到這里,整個(gè)程序執(zhí)行完畢。
看了分析之后,有沒(méi)有一種豁然開(kāi)朗的感覺(jué)呢??!
例子二
class Grandpa {
static {
System.out.println("Grandpa靜態(tài)代碼塊");
}
public Grandpa() {
System.out.println("我是爺爺");
}
}
class Father extends Grandpa {
static {
System.out.println("Father靜態(tài)代碼塊");
}
public Father() {
System.out.println("我是爸爸");
}
static int age = 26;
}
class Son extends Father {
static {
System.out.println("Son靜態(tài)代碼塊");
}
public Son() {
System.out.println("我是兒子");
}
}
public class SonTest {
public static void main(String[] args) {
System.out.println("爸爸的歲數(shù): " + Son.age);
new Son();
}
}
理解了第一個(gè)例子,相信這個(gè)例子就難不倒大家了,直接公布答案吧:
Grandpa靜態(tài)代碼塊
Father靜態(tài)代碼塊
爸爸的歲數(shù): 26
Son靜態(tài)代碼塊
我是爺爺
我是爸爸
我是兒子
解釋?zhuān)撼绦蛉肟跒閙ain方法,首先會(huì)初始化SonTest類(lèi),而SonTest中并沒(méi)有內(nèi)容,所以不用管,直接進(jìn)入main方法中,調(diào)用Son.age,而age是父類(lèi)中的靜態(tài)字段,就會(huì)直接初始化父類(lèi)Father,而不會(huì)初始化子類(lèi)(規(guī)則:對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化)。
初始化Father的時(shí)候,發(fā)現(xiàn)它繼承Grandpa,而Grandpa此時(shí)也還沒(méi)有初始化,所以此時(shí)先初始化Grandpa。Grandpa類(lèi)構(gòu)造器中只有一段靜態(tài)代碼塊,會(huì)輸出Grandpa靜態(tài)代碼塊,然后初始化Father構(gòu)造器,輸出Father構(gòu)造器,接著就會(huì)輸出main方法中的第一句爸爸的歲數(shù)為:26。
接著是實(shí)例化Son對(duì)象,調(diào)用Son的對(duì)象構(gòu)造方法,會(huì)先調(diào)用父類(lèi)Father對(duì)象構(gòu)造方法,而Father又繼承Grandpa,所以先調(diào)用Grandpa對(duì)象構(gòu)造方法,所以最后一次輸出我是爺爺,我是爸爸,我是兒子。
到這里,相信大家知道怎樣分析這類(lèi)問(wèn)題??偨Y(jié)一個(gè)方法論:
-
確定類(lèi)變量的初始值:在類(lèi)加載準(zhǔn)備階段,JVM為類(lèi)變量(靜態(tài)變量)初始化默認(rèn)值,如果被final修飾,則會(huì)直接初始化用戶(hù)賦予的值。 -
初始化入口方法:進(jìn)入類(lèi)加載初始化階段,JVM會(huì)尋找main方法入口,從而初始化main方法所在的類(lèi),當(dāng)需要初始化一個(gè)類(lèi)時(shí),先初始化類(lèi)構(gòu)造器,之后初始化對(duì)象構(gòu)造器。 -
初始化類(lèi)構(gòu)造器:JVM按照順序收集類(lèi)變量賦值語(yǔ)句,靜態(tài)代碼塊,最終組成類(lèi)構(gòu)造器,JVM執(zhí)行這個(gè)類(lèi)構(gòu)造器。 -
初始化對(duì)象構(gòu)造器:JVM按照順序收集成員變量賦值語(yǔ)句,普通代碼塊,最后收集對(duì)象構(gòu)造方法,經(jīng)他們組成對(duì)象構(gòu)造器,最終由JVM執(zhí)行。
總結(jié)
本文先介紹類(lèi)加載機(jī)制的原理,然后舉例2個(gè),幫助大家應(yīng)用原理來(lái)分析具體場(chǎng)景,最后總結(jié)一套方法論,如果遇到同樣的問(wèn)題,可以使用這套方法論來(lái)解決。希望對(duì)大家有幫助。