Java JVM類加載機(jī)制解讀
1.什么是類加載
首先你要知道一個類的從被加載到虛擬機(jī)內(nèi)存中開始,到被初始化為止,是為類加載的整個過程。下圖就是類加載的整個過程:

一個類只有經(jīng)歷了加載、驗證、準(zhǔn)備、解析、初始化這五個關(guān)卡才能被認(rèn)為是實(shí)現(xiàn)了類加載。這,就是類加載。
注意一點(diǎn):上面五個過程并不是按部就班地“完成”,而是按部就班地“執(zhí)行”(除解析過程外)。執(zhí)行時一定是先開始加載,再開始驗證,但加載過程中也可能會直接開始驗證。
2.類加載的過程
2.1加載
“加載”只是是“類加載”過程的第一個階段,關(guān)于在什么時候開始,規(guī)范并沒有進(jìn)行強(qiáng)制約束,可以讓虛擬機(jī)自行把握。在這個階段中,Java虛擬機(jī)需要完成以下三件事:
1)通過一個類的全限定名來獲取這個類的二進(jìn)制字節(jié)流
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的方問入口
可以用一句話概括:加載是一個讀取Class文件,將其轉(zhuǎn)化為某種靜態(tài)數(shù)據(jù)結(jié)構(gòu)存儲在方法區(qū)內(nèi),并在堆中生成一個便于用戶調(diào)用的java.lang.Class類型的對象的過程

2.2驗證
驗證是連接階段的第一步,這個階段的目的是確保Class文件的字節(jié)流中包含的信息符合約束要求,,保證這些信息被當(dāng)做代碼運(yùn)行后不會危害虛擬機(jī)自身的安全。
這一過程了解即可。
2.3準(zhǔn)備
準(zhǔn)備階段是正式為類中定義的變量(這里說的是靜態(tài)變量,也就是被static修飾的變量)分配內(nèi)存,并設(shè)置類變量初始值的階段。
這里有兩點(diǎn)需要強(qiáng)調(diào):
1)首先這里進(jìn)行內(nèi)存分配的僅僅是類變量,而不包括實(shí)例變量,實(shí)例變量將會在對象實(shí)例化時隨著對象一起分配在Java堆中。
2)其次這里設(shè)置的初始值“通常情況”下是數(shù)據(jù)的零值,而不是用戶本身對它賦的初值。
如下代碼:
public static int a = 10;
變量a在準(zhǔn)備階段后的初始值是0,而不是10,因為現(xiàn)在只是在類加載過程中,還沒有執(zhí)行任何方法。
上面說到“通常情況”,那就說明還有特殊情況咯,加修飾詞final時:
public static final int a = 10;
這時在準(zhǔn)備階段虛擬機(jī)就會將a設(shè)置為10。其實(shí)也不難理解:我們將它設(shè)置為常量,那就肯定在任何時候都不能修改啊,天子犯法與庶民同罪!
2.4解析
解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程,這一過程也可能在初始化后進(jìn)行,并不一定和流程圖的執(zhí)行順序一樣。
符號引用:符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。
直接引用:直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。
這一過程比較復(fù)雜,有興趣可以參考《深入理解Java虛擬機(jī)》
2.5初始化【重中之重之重中重】
類的初始化階段是類加載過程的最后一個階段。在這個階段Java虛擬機(jī)才開始真正執(zhí)行類中編寫的Java程序代碼。
初始化階段有以下六種情況必須立即對類進(jìn)行“初始化”:
- 1)使用new關(guān)鍵字實(shí)例化對象的時候
- 2)讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候
- 3)調(diào)用一個類的靜態(tài)方法的時候
- 4)使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 5)當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 6)當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機(jī)會先初始化這個主類。
光說不行,主要看
第一段代碼:
package com.bit.JVMTest;
class Father {
public static int a = 10;
static {
System.out.println("爸爸靜態(tài)代碼塊");
}
}
class Son extends Father{
public static int b = 20;
static {
System.out.println("兒子靜態(tài)代碼塊");
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Son.b);
}
}
運(yùn)行結(jié)果:
爸爸靜態(tài)代碼塊
兒子靜態(tài)代碼塊
20
首先Son.b是在讀取Son類自己的靜態(tài)字段,這點(diǎn)符合上面六中情況的第二種:讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候需要進(jìn)行初始化。
其次Son類繼承Father類,也就符合第五條:當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化,所以我們先初始化的應(yīng)該是Father類,然后是Son類。
因此,打印的內(nèi)容首先是爸爸靜態(tài)代碼塊(父類先初始化),然后是兒子靜態(tài)代碼塊(子類再初始化),最后是我們想要打印的b(20)本身。
再看
第二段代碼:
package com.bit.JVMTest;
class grandFather{
static{
System.out.println("爺爺靜態(tài)代碼塊");
}
}
class Father extends grandFather{
public static int a = 10;
static {
System.out.println("爸爸靜態(tài)代碼塊");
}
}
class Son extends Father{
public static int b = 20;
static {
System.out.println("兒子靜態(tài)代碼塊");
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Son.a);
}
}
運(yùn)行結(jié)果:
爺爺靜態(tài)代碼塊
爸爸靜態(tài)代碼塊
10
首先要明確:Son.a是在讀取父類Father類的靜態(tài)字段(注意a字段在Son類的父類中),而不是讀取Son類本身的靜態(tài)字段
因此這次不會初始化Son類本身。
因此這次不會初始化Son類本身。
因此這次不會初始化Son類本身。
其它的和第一段代碼很相似:JVM在初始化Father類的時候,發(fā)現(xiàn)這個類還有一個父類沒有被初始化,那就先初始化它的父類:grandFather。
因此,打印的內(nèi)容首先是爺爺靜態(tài)代碼塊(Father類的父類先初始化),然后是爸爸靜態(tài)代碼塊(Father類再初始化),最后是我們想要打印的a(10)本身。
第三段代碼:
package com.bit.JVMTest;
class grandFather{
static{
System.out.println("爺爺靜態(tài)代碼塊");
}
}
class Father extends grandFather{
public final static int a = 10;
static {
System.out.println("爸爸靜態(tài)代碼塊");
}
}
class Son extends Father{
public static int b = 20;
static {
System.out.println("兒子靜態(tài)代碼塊");
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Son.a);
}
}
運(yùn)行結(jié)果:10
看到這里是不是想說臥**你*個*。
別急別急,這里的主函數(shù)調(diào)用雖然和第二段代碼一樣,但是注意?。?!我們給a這個靜態(tài)字段加了一個final修飾符。
再看六條中的第(2)條:讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候會觸發(fā)類加載。
也就是說我們讀取的a是被final修飾的,讀取這種靜態(tài)字段并不會引起任何類的初始化,所以就直接打印a(10)了。
再看
最后一段代碼:
package com.bit.JVMTest;
class Father {
public Father(){
System.out.println("爸爸構(gòu)造方法");
}
static {
System.out.println("爸爸靜態(tài)代碼塊");
}
{
System.out.println("爸爸普通代碼塊");
}
}
class Son extends Father{
public Son(){
System.out.println("兒子構(gòu)造方法");
}
static {
System.out.println("兒子靜態(tài)代碼塊");
}
{
System.out.println("兒子普通代碼塊");
}
}
public class ClassLoaderTest extends Son{
public static void main(String[] args) {
System.out.println("開始");
new Son();//這里實(shí)例化一個Son類的對象
System.out.println("結(jié)束");
}
}
運(yùn)行結(jié)果:
爸爸靜態(tài)代碼塊
兒子靜態(tài)代碼塊
開始
爸爸普通代碼塊
爸爸構(gòu)造方法
兒子普通代碼塊
兒子構(gòu)造方法
結(jié)束
看到這里是不是欲哭無淚,我**不學(xué)了我。別急先聽我細(xì)細(xì)分析一波~
這里有一個細(xì)節(jié):主類繼承了Son類!,這貌似沒什么啊,但是還有一個細(xì)節(jié):我們的main()方法是主類中的靜態(tài)方法!看到這里是不是明白了些什么?
沒錯!當(dāng)我們調(diào)用main()方法的時候,就引起了主類的初始化,主類繼承Son類,Son類繼承Father類,所以就先進(jìn)行Father類的初始化:打印爸爸靜態(tài)代碼塊,接著Son類初始化:打印兒子靜態(tài)代碼塊,最后該終于我主類初始化了:代碼中沒什么可以初始化的…(尷尬)。
接下來是第二階段:執(zhí)行main()方法:
1.先打?。洪_始字樣。
2.接著是構(gòu)造 Son()實(shí)例,那么就會先構(gòu)造它的父類Father()的實(shí)例:構(gòu)造實(shí)例時按照先執(zhí)行代碼塊,再執(zhí)行構(gòu)造方法的順序來。所以就先打印了:爸爸普通代碼塊、爸爸構(gòu)造方法 這幾個大字。然后再執(zhí)行構(gòu)造Son()的實(shí)例,構(gòu)造順序一樣,所以就后打印了:兒子普通代碼塊、兒子構(gòu)造方法 這幾個大字。
3.最后打?。航Y(jié)束字樣。
此時main()才方法真正結(jié)束。
總結(jié)
我們平常所說的類加載體現(xiàn)在代碼上就是初始化這一階段,我這里結(jié)束的也僅限于此,想了解詳細(xì)的類加載可以參考《深入理解Java虛擬機(jī)》這本書,也可以看其他博主的知識總結(jié)。感謝你能看到這里!
到此這篇關(guān)于Java JVM類加載機(jī)制解讀的文章就介紹到這了,更多相關(guān)Java JVM 類加載機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring事務(wù)傳播中嵌套調(diào)用實(shí)現(xiàn)方法詳細(xì)介紹
Spring事務(wù)的本質(zhì)就是對數(shù)據(jù)庫事務(wù)的支持,沒有數(shù)據(jù)庫事務(wù),Spring是無法提供事務(wù)功能的。Spring只提供統(tǒng)一的事務(wù)管理接口,具體實(shí)現(xiàn)都是由數(shù)據(jù)庫自己實(shí)現(xiàn)的,Spring會在事務(wù)開始時,根據(jù)當(dāng)前設(shè)置的隔離級別,調(diào)整數(shù)據(jù)庫的隔離級別,由此保持一致2022-11-11
在java8中使用流區(qū)分質(zhì)數(shù)與非質(zhì)數(shù)詳解
這篇文章主要介紹了在java8中使用流區(qū)分質(zhì)數(shù)與非質(zhì)數(shù)詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
如何將Spring Session存儲到Redis中實(shí)現(xiàn)持久化
這篇文章主要介紹了如何將Spring Session存儲到Redis中實(shí)現(xiàn)持久化,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07
Java下載文件中文文件名亂碼的解決方案(文件名包含很多%)
Java下載文件時,文件名中文亂碼問題通常是由于編碼不正確導(dǎo)致的,使用`URLEncoder.encode(filepath, "UTF-8")`可以解決在提示下載框中正確顯示漢字文件名的問題,但在選擇直接打開時,文件名會變成亂碼,解決這個問題的方法2025-02-02
java并發(fā)編程之深入理解Synchronized的使用
文詳細(xì)講述了線程、進(jìn)程的關(guān)系及在操作系統(tǒng)中的表現(xiàn),這是多線程學(xué)習(xí)必須了解的基礎(chǔ)。本文將接著講一下Java線程同步中的一個重要的概念synchronized,希望能夠給你有所幫助2021-06-06
SpringBoot整合mybatis-generator插件流程詳細(xì)講解
這篇文章主要介紹了SpringBoot整合mybatis-generator插件流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02
SpringBoot靜態(tài)視頻實(shí)時播放的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringBoot靜態(tài)視頻實(shí)時播放的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01

