Java JVM類加載機(jī)制解讀
1.什么是類加載
首先你要知道一個(gè)類的從被加載到虛擬機(jī)內(nèi)存中開始,到被初始化為止,是為類加載的整個(gè)過程。下圖就是類加載的整個(gè)過程:
一個(gè)類只有經(jīng)歷了加載、驗(yàn)證、準(zhǔn)備、解析、初始化這五個(gè)關(guān)卡才能被認(rèn)為是實(shí)現(xiàn)了類加載。這,就是類加載。
注意一點(diǎn):上面五個(gè)過程并不是按部就班地“完成”,而是按部就班地“執(zhí)行”(除解析過程外)。執(zhí)行時(shí)一定是先開始加載,再開始驗(yàn)證,但加載過程中也可能會(huì)直接開始驗(yàn)證。
2.類加載的過程
2.1加載
“加載”只是是“類加載”過程的第一個(gè)階段,關(guān)于在什么時(shí)候開始,規(guī)范并沒有進(jìn)行強(qiáng)制約束,可以讓虛擬機(jī)自行把握。在這個(gè)階段中,Java虛擬機(jī)需要完成以下三件事:
1)通過一個(gè)類的全限定名來獲取這個(gè)類的二進(jìn)制字節(jié)流
2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
3)在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的方問入口
可以用一句話概括:加載是一個(gè)讀取Class文件,將其轉(zhuǎn)化為某種靜態(tài)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在方法區(qū)內(nèi),并在堆中生成一個(gè)便于用戶調(diào)用的java.lang.Class類型的對(duì)象的過程
2.2驗(yàn)證
驗(yàn)證是連接階段的第一步,這個(gè)階段的目的是確保Class文件的字節(jié)流中包含的信息符合約束要求,,保證這些信息被當(dāng)做代碼運(yùn)行后不會(huì)危害虛擬機(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í)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。
2)其次這里設(shè)置的初始值“通常情況”下是數(shù)據(jù)的零值,而不是用戶本身對(duì)它賦的初值。
如下代碼:
public static int a = 10;
變量a在準(zhǔn)備階段后的初始值是0,而不是10,因?yàn)楝F(xiàn)在只是在類加載過程中,還沒有執(zhí)行任何方法。
上面說到“通常情況”,那就說明還有特殊情況咯,加修飾詞final
時(shí):
public static final int a = 10;
這時(shí)在準(zhǔn)備階段虛擬機(jī)就會(huì)將a設(shè)置為10。其實(shí)也不難理解:我們將它設(shè)置為常量,那就肯定在任何時(shí)候都不能修改啊,天子犯法與庶民同罪!
2.4解析
解析階段是Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程,這一過程也可能在初始化后進(jìn)行,并不一定和流程圖的執(zhí)行順序一樣。
符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。
直接引用:直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄。
這一過程比較復(fù)雜,有興趣可以參考《深入理解Java虛擬機(jī)》
2.5初始化【重中之重之重中重】
類的初始化階段是類加載過程的最后一個(gè)階段。在這個(gè)階段Java虛擬機(jī)才開始真正執(zhí)行類中編寫的Java程序代碼。
初始化階段有以下六種情況必須立即對(duì)類進(jìn)行“初始化”:
- 1)使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候
- 2)讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候
- 3)調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
- 4)使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
- 5)當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
- 6)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。
光說不行,主要看
第一段代碼:
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è)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候需要進(jìn)行初始化。
其次Son
類繼承Father
類,也就符合第五條:當(dāng)初始化一個(gè)類的時(shí)候,如果發(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)字段
因此這次不會(huì)初始化Son類本身。
因此這次不會(huì)初始化Son類本身。
因此這次不會(huì)初始化Son類本身。
其它的和第一段代碼很相似:JVM在初始化Father
類的時(shí)候,發(fā)現(xiàn)這個(gè)類還有一個(gè)父類沒有被初始化,那就先初始化它的父類: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
看到這里是不是想說臥**你*個(gè)*。
別急別急,這里的主函數(shù)調(diào)用雖然和第二段代碼一樣,但是注意?。?!我們給a這個(gè)靜態(tài)字段加了一個(gè)final修飾符
。
再看六條中的第(2)條:讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候會(huì)觸發(fā)類加載。
也就是說我們讀取的a是被final修飾的,讀取這種靜態(tài)字段并不會(huì)引起任何類的初始化,所以就直接打印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í)例化一個(gè)Son類的對(duì)象 System.out.println("結(jié)束"); } }
運(yùn)行結(jié)果:
爸爸靜態(tài)代碼塊
兒子靜態(tài)代碼塊
開始
爸爸普通代碼塊
爸爸構(gòu)造方法
兒子普通代碼塊
兒子構(gòu)造方法
結(jié)束
看到這里是不是欲哭無淚,我**不學(xué)了我。別急先聽我細(xì)細(xì)分析一波~
這里有一個(gè)細(xì)節(jié):主類繼承了Son類!,這貌似沒什么啊,但是還有一個(gè)細(xì)節(jié):我們的main()方法是主類中的靜態(tài)方法!看到這里是不是明白了些什么?
沒錯(cuò)!當(dāng)我們調(diào)用main()
方法的時(shí)候,就引起了主類的初始化,主類繼承Son類
,Son類繼承Father
類,所以就先進(jìn)行Father類的初始化:打印爸爸靜態(tài)代碼塊,接著Son類初始化:打印兒子靜態(tài)代碼塊,最后該終于我主類初始化了:代碼中沒什么可以初始化的…(尷尬)。
接下來是第二階段:執(zhí)行main()
方法:
1.先打?。洪_始字樣。
2.接著是構(gòu)造 Son()
實(shí)例,那么就會(huì)先構(gòu)造它的父類Father()
的實(shí)例:構(gòu)造實(shí)例時(shí)按照先執(zhí)行代碼塊,再執(zhí)行構(gòu)造方法的順序來。所以就先打印了:爸爸普通代碼塊、爸爸構(gòu)造方法 這幾個(gè)大字。然后再執(zhí)行構(gòu)造Son()的實(shí)例,構(gòu)造順序一樣,所以就后打印了:兒子普通代碼塊、兒子構(gòu)造方法 這幾個(gè)大字。
3.最后打?。航Y(jié)束字樣。
此時(shí)main()才方法真正結(jié)束。
總結(jié)
我們平常所說的類加載體現(xiàn)在代碼上就是初始化這一階段,我這里結(jié)束的也僅限于此,想了解詳細(xì)的類加載可以參考《深入理解Java虛擬機(jī)》這本書,也可以看其他博主的知識(shí)總結(jié)。感謝你能看到這里!
到此這篇關(guān)于Java JVM類加載機(jī)制解讀的文章就介紹到這了,更多相關(guān)Java JVM 類加載機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
結(jié)合線程池實(shí)現(xiàn)apache?kafka消費(fèi)者組的誤區(qū)及解決方法
這篇文章主要介紹了結(jié)合線程池實(shí)現(xiàn)apache?kafka消費(fèi)者組的誤區(qū)及解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Java中生成隨機(jī)數(shù)的4種方式與區(qū)別詳解
生成隨機(jī)數(shù)是我們?nèi)粘i_發(fā)經(jīng)常會(huì)遇到的一個(gè)功能,這篇文章主要給大家介紹了關(guān)于Java中生成隨機(jī)數(shù)的4種方式與區(qū)別、應(yīng)用場景的相關(guān)資料,4個(gè)方式分別是Random、ThreadLocalRandom、SecureRandom以及Math,需要的朋友可以參考下2021-06-06如何基于java隨機(jī)獲取不重復(fù)數(shù)值
這篇文章主要介紹了如何基于java隨機(jī)獲取不重復(fù)數(shù)值,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09struts2標(biāo)簽總結(jié)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)總結(jié)了struts2標(biāo)簽的使用方法,和學(xué)習(xí)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09java中把漢字轉(zhuǎn)換成簡拼的實(shí)現(xiàn)代碼
本篇文章是對(duì)在java中把漢字轉(zhuǎn)換成簡拼的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05