掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流
我們寫(xiě)的類(lèi),在編譯完成后,究竟是怎么加載進(jìn)虛擬機(jī)的?虛擬機(jī)又做了什么神奇操作?本文可以帶著讀者初探類(lèi)加載機(jī)制。上來(lái)先放類(lèi)加載各個(gè)階段的主要任務(wù),用于給讀者一個(gè)大概的印象體驗(yàn),現(xiàn)在記不住也沒(méi)有什么關(guān)系。

為興安盟烏蘭浩特等地區(qū)用戶(hù)提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及興安盟烏蘭浩特網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作、興安盟烏蘭浩特網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專(zhuān)業(yè)、用心的態(tài)度為用戶(hù)提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶(hù)的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!
現(xiàn)在只需要記住三個(gè)名詞,裝載——>連接——>初始化,記住了嗎,我們要開(kāi)始奇幻漂流了!
在文章的最后,我們使用幾個(gè)例子來(lái)加深對(duì)程序執(zhí)行順序的理解。
1. 裝載
我覺(jué)得這里使用裝載更好一點(diǎn),第一,可以避免與類(lèi)加載過(guò)程中的“加載”混淆,第二,裝載體現(xiàn)的就是一個(gè)“裝”字,僅僅是把貨物從一個(gè)地方搬到另外一個(gè)地方而已,而這里的加載,卻包含搬運(yùn)貨物、處理貨物等一系列流程。
裝載階段,將.class字節(jié)碼文件的二進(jìn)制數(shù)據(jù)讀入內(nèi)存中,然后將這些數(shù)據(jù)翻譯成類(lèi)的元數(shù)據(jù),元數(shù)據(jù)包括方法代碼,變量名,方法名,訪問(wèn)權(quán)限與返回值,接著將元數(shù)據(jù)存入方法區(qū)。最后會(huì)在堆中創(chuàng)建一個(gè)Class對(duì)象,用來(lái)封裝類(lèi)在方法區(qū)中的數(shù)據(jù)結(jié)構(gòu),因此我們可以通過(guò)訪問(wèn)此Class對(duì)象,來(lái)間接訪問(wèn)方法區(qū)中的元數(shù)據(jù)。
在Java7與Java8之后,方法區(qū)有不同的實(shí)現(xiàn),這部分詳細(xì)內(nèi)容可以參考我的另外一篇博客靈性一問(wèn)——為什么用元空間替換永久代?
總結(jié)來(lái)講,裝載的子流程為:
.class文件讀入內(nèi)存——>元數(shù)據(jù)放進(jìn)方法區(qū)——>Class對(duì)象放進(jìn)堆中
最后我們?cè)L問(wèn)此Class對(duì)象,即可獲取該類(lèi)在方法區(qū)中的結(jié)構(gòu)。
2. 連接
連接又包括驗(yàn)證、準(zhǔn)備、初始化
2.1 驗(yàn)證
驗(yàn)證被加載類(lèi)的正確性與安全性,看class文件是否正確,是否對(duì)會(huì)對(duì)虛擬機(jī)造成安全問(wèn)題等,主要去驗(yàn)證文件格式、元數(shù)據(jù)、字節(jié)碼與符合引用。
2.1.1 驗(yàn)證文件格式
2.1.1.1 驗(yàn)證文件類(lèi)型
每個(gè)文件都有特定的類(lèi)型,類(lèi)型標(biāo)識(shí)字段存在于文件的開(kāi)頭中,采用16進(jìn)制表示,類(lèi)型標(biāo)識(shí)字段稱(chēng)為魔數(shù),class文件的魔數(shù)為0xCAFEBABY,關(guān)于此魔數(shù)的由來(lái)也很有意思,可以看這篇文章class文件魔數(shù)CAFEBABE的由來(lái)。
2.1.1.2 驗(yàn)證主次版本號(hào)
檢查看主次版本號(hào)是否在當(dāng)前jvm處理的范圍之內(nèi),主次版本號(hào)的存放位置緊隨在魔數(shù)之后。
2.1.1.3 驗(yàn)證常量池
常量池是class文件中最為復(fù)雜的一部分,對(duì)常量池的驗(yàn)證主要是驗(yàn)證常量池中是否有不支持的類(lèi)型。
例如,有以下簡(jiǎn)答的代碼:
- public class Main {
- public static void main(String[] args) {
- int a=1;
- int b=2;
- int c=a+b;
- }
- }
在該類(lèi)的路徑下,使用javac Main.java編譯,然后使用javap -v Main可以輸出以下信息:
以上標(biāo)紅處,就是class文件中存儲(chǔ)常量池的地方。
2.1.2 驗(yàn)證元數(shù)據(jù)
主要是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合java語(yǔ)言規(guī)范的要求,比如說(shuō)驗(yàn)證這個(gè)類(lèi)是不是有父類(lèi),類(lèi)中的字段方法是不是和父類(lèi)沖突等等。
2.1.3 驗(yàn)證字節(jié)碼
這是整個(gè)驗(yàn)證過(guò)程最復(fù)雜的階段,主要是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
2.1.4 驗(yàn)證符號(hào)引用
它是驗(yàn)證的最后一個(gè)階段,發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候。主要是對(duì)類(lèi)自身以外的信息進(jìn)行校驗(yàn)。目的是確保解析動(dòng)作能夠完成。
對(duì)整個(gè)類(lèi)加載機(jī)制而言,驗(yàn)證階段是一個(gè)很重要但是非必需的階段,如果我們的代碼能夠確保沒(méi)有問(wèn)題,那么就沒(méi)有必要去驗(yàn)證,畢竟驗(yàn)證需要花費(fèi)一定的的時(shí)間,可以使用-Xverfity:none來(lái)關(guān)閉大部分的驗(yàn)證。
2.2 準(zhǔn)備
在這個(gè)階段中,主要是為類(lèi)變量(靜態(tài)變量)分配內(nèi)存以及初始化默認(rèn)值,因?yàn)殪o態(tài)變量全局只有一份,是跟著類(lèi)走的,因此分配內(nèi)存其實(shí)是在方法區(qū)上分配。
這里有3個(gè)注意點(diǎn):
(1)在準(zhǔn)備階段,虛擬機(jī)只為靜態(tài)變量分配內(nèi)存,實(shí)例變量要等到初始化階段才開(kāi)始分配內(nèi)存。這個(gè)時(shí)候還沒(méi)有實(shí)例化該類(lèi),連對(duì)象都沒(méi)有,因此這個(gè)時(shí)候還不存在實(shí)例變量。
(2)為靜態(tài)變量初始化默認(rèn)值,注意,是初始化對(duì)應(yīng)數(shù)據(jù)類(lèi)型的默認(rèn)值,不是自定義的值。
例如,代碼中是這樣寫(xiě)的,自定義int類(lèi)型的變量a的值為1
- private static int a=1;
但是,在準(zhǔn)備階段完成之后,a的值只會(huì)被初始化為0,而不是1。
(3)被final修飾的靜態(tài)變量,如果值比較小,則在編譯后直接內(nèi)嵌到字節(jié)碼中。如果值比較大,也是在編譯后直接放入常量池中。因此,準(zhǔn)備階段結(jié)束后,final類(lèi)型的靜態(tài)變量已經(jīng)有了用戶(hù)自定義的值,而不是默認(rèn)值。
2.3 解析
解析階段,主要是將class文件中常量池中的符號(hào)引用轉(zhuǎn)化為直接引用。
符號(hào)引用的含義:
可以直接理解為是一個(gè)字符串,用這個(gè)字符串來(lái)表示一個(gè)目標(biāo)。就像博主的名字是SunAlwaysOnline,這個(gè)SunAlwaysOnline字符串就是一個(gè)符號(hào)引用,代表博主,但是現(xiàn)在不能通過(guò)名字直接找到我本人。
直接引用的含義:
直接引用是一個(gè)指向目標(biāo)的指針,能夠通過(guò)直接引用定位到目標(biāo)。比如
- Student s=new Student();
我們可以通過(guò)引用變量s直接定位到新創(chuàng)建出的Student對(duì)象實(shí)例。
將符號(hào)引用轉(zhuǎn)化為直接引用,就能將平淡無(wú)奇的字符串轉(zhuǎn)化為指向?qū)ο蟮闹羔槨?/p>
3. 初始化
執(zhí)行初始化,就是虛擬機(jī)執(zhí)行類(lèi)構(gòu)造器 ()方法的過(guò)程, ()方法是由編譯器自動(dòng)去搜集類(lèi)中的所有類(lèi)變量與靜態(tài)語(yǔ)句塊合并產(chǎn)生的??赡艽嬖诙鄠€(gè)線(xiàn)程同時(shí)執(zhí)行某個(gè)類(lèi)的 ()方法,虛擬機(jī)此時(shí)會(huì)對(duì)該方法進(jìn)行加鎖,保證只有一個(gè)線(xiàn)程能執(zhí)行。
到了這個(gè)階段,類(lèi)變量與類(lèi)成員變量才會(huì)被賦予用戶(hù)自定義的值。
當(dāng)然,一個(gè)類(lèi)并不是被初始化多次,只有當(dāng)對(duì)類(lèi)的首次主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類(lèi)的初始化。主動(dòng)使用包含以下幾種方式:
被動(dòng)使用會(huì)發(fā)生呢?
4. 類(lèi)的初始化順序
現(xiàn)在有以下的代碼:
- class Father {
- public static int fatherA = 1;
- public static final int fatherB = 2;
- static {
- System.out.println("父類(lèi)的靜態(tài)代碼塊");
- }
- {
- System.out.println("父類(lèi)的非靜態(tài)代碼塊");
- }
- Father() {
- System.out.println("父類(lèi)的構(gòu)造方法");
- }
- }
- class Son extends Father {
- public static int sonA = 3;
- public static final int sonB = 4;
- static {
- System.out.println("子類(lèi)的靜態(tài)代碼塊");
- }
- {
- System.out.println("子類(lèi)的非靜態(tài)代碼塊");
- }
- Son() {
- System.out.println("子類(lèi)的構(gòu)造方法");
- }
- }
(1)Main方法中實(shí)例化子類(lèi):
- public class Main {
- public static void main(String[] args) {
- Son son = new Son();
- }
- }
首先可以確定的是,這屬于主動(dòng)使用,父類(lèi)先于子類(lèi)初始化,因此會(huì)得到以下的輸出:
這里可以進(jìn)行總結(jié),程序執(zhí)行的順序?yàn)椋?/p>
父類(lèi)的靜態(tài)域->子類(lèi)的靜態(tài)域->父類(lèi)的非靜態(tài)域->子類(lèi)的非靜態(tài)域->父類(lèi)的構(gòu)造方法->子類(lèi)的構(gòu)造方法
這里的靜態(tài)域包括靜態(tài)變量與靜態(tài)代碼塊,靜態(tài)變量和靜態(tài)代碼塊的執(zhí)行順序由編碼順序決定。
規(guī)律就是,靜態(tài)先于非靜態(tài),父類(lèi)先于子類(lèi),構(gòu)造方法在最后。嗯給我背三遍
(2)Mian方法中輸出子類(lèi)的sonA屬性
- public class Main {
- public static void main(String[] args) {
- System.out.println(Son.sonA);
- }
- }
這里只要輸出子類(lèi)的靜態(tài)屬性sonA,因此需要初始化子類(lèi),但父類(lèi)還沒(méi)有被初始化,因此先初始化父類(lèi)。一般而言,靜態(tài)代碼塊會(huì)對(duì)靜態(tài)變量進(jìn)行賦值,因此調(diào)用靜態(tài)屬性,在此之前虛擬機(jī)會(huì)調(diào)用靜態(tài)代碼塊。所以,輸出如下:
(3)Main方法輸出子類(lèi)繼承而來(lái)的fatherA屬性
- public class Main {
- public static void main(String[] args) {
- System.out.println(Son.fatherA);
- }
- }
子類(lèi)從父類(lèi)繼承而來(lái)的屬性,因此這里屬于被動(dòng)使用。只會(huì)執(zhí)行靜態(tài)屬性真正存在的那個(gè)類(lèi)的初始化,即只會(huì)初始化父類(lèi)。因此,輸出:
(4)Main方法中聲明并創(chuàng)建一個(gè)子類(lèi)類(lèi)型的數(shù)組
- public class Main {
- public static void main(String[] args) {
- Son[] sons=new Son[10];
- }
- }
顯然,這屬于被動(dòng)使用,不會(huì)初始化Son類(lèi)。因此,沒(méi)有任何輸出。
(5)Main方法輸出子類(lèi)被static final修飾的變量
- public class Main {
- public static void main(String[] args) {
- System.out.println(Son.sonB);
- }
- }
顯然,被static final修改的變量,也就是一個(gè)常量,在編譯器就放入類(lèi)的常量池中了,不需要初始化類(lèi)。因此,這里只輸出sonB的值,即為4。
(6)在聲明前使用一個(gè)靜態(tài)變量
- public class Main {
- static {
- c = 1;
- }
- public static int c;
- }
這樣的代碼,是可以運(yùn)行的,小朋友,你是不是有大大的疑問(wèn)?但容我自仔細(xì)分析來(lái)。
首先,在準(zhǔn)備階段,為靜態(tài)變量c分配內(nèi)存,然后賦予初始值0。等到初始化階段,執(zhí)行類(lèi)的靜態(tài)域,也就是執(zhí)行此處的靜態(tài)代碼塊中c=1,c此時(shí)已經(jīng)存在,也有了一個(gè)默認(rèn)值0,此時(shí)可以修改c的值。
但是,如果我僅僅在c=1后使用c的話(huà),如:
- public class Main {
- static {
- c = 1;
- System.out.println(c);
- }
- public static int c;
- }
此時(shí)編譯沒(méi)法通過(guò),編輯器提示Illegal forward reference,即非法前向引用,似乎只能寫(xiě)入c,不能讀取c。我們之前已經(jīng)分析過(guò)了,此時(shí)在內(nèi)存中是有這個(gè)c的,那為什么不能讀取c?
本來(lái)在正常的情況下,要想使用一個(gè)變量,變量首先需要聲明出來(lái)。當(dāng)然,java做出了一種特許,允許在使用前不先聲明,但必須要滿(mǎn)足幾個(gè)條件,其中有一個(gè)條件是該變量只能出現(xiàn)在賦值表達(dá)式的左邊,即c=1可以,c=2可以,c+=1不可以(c+=1也就是c=c+1,違反了左值協(xié)定)。當(dāng)然如果這里使用全限定名,也就是輸出Main.c時(shí),則可以正常運(yùn)行。
有的小伙伴可能還是有大大的疑問(wèn),不要緊,沒(méi)看懂的可以參考以下講解非法前向引用的文章
java報(bào)錯(cuò)非法的前向引用問(wèn)題
Java編譯時(shí)提示非法向前引用
Illegal forward Reference java issue
關(guān)于加載使用到的類(lèi)加載器,雙親委派機(jī)制,如何自定義類(lèi)加載器,可能需要另開(kāi)篇幅。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流