您好,登錄后才能下訂單哦!
Java中是如何加載類的?針對這個問題,這篇文章詳細(xì)介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
Java類加載全過程
一個java文件從被加載到被卸載這個生命過程,總共要經(jīng)歷4個階段:
加載->鏈接(驗證+準(zhǔn)備+解析)->初始化(使用前的準(zhǔn)備)->使用->卸載
其中加載(除了自定義加載)+鏈接的過程是完全由jvm負(fù)責(zé)的,什么時候要對類進(jìn)行初始化工作(加載+鏈接在此之前已經(jīng)完成了),jvm有嚴(yán)格的規(guī)定(四種情況):
1.遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,加入類還沒進(jìn)行初始化,則馬上對其進(jìn)行初始化工作。其實就是3種情況:用new實例化一個類時、讀取或者設(shè)置類的靜態(tài)字段時(不包括被final修飾的靜態(tài)字段,因為他們已經(jīng)被塞進(jìn)常量池了)、以及執(zhí)行靜態(tài)方法的時候。
2.使用java.lang.reflect.*的方法對類進(jìn)行反射調(diào)用的時候,如果類還沒有進(jìn)行過初始化,馬上對其進(jìn)行。
3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
4.當(dāng)jvm啟動時,用戶需要指定一個要執(zhí)行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
以上4種預(yù)處理稱為對一個類進(jìn)行主動的引用,其余的其他情況,稱為被動引用,都不會觸發(fā)類的初始化。下面也舉了些被動引用的例子:
/** * 被動引用情景1 * 通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類的初始化 * @author volador * */ class SuperClass{ static{ System.out.println("super class init."); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("sub class init."); } } public class test{ public static void main(String[]args){ System.out.println(SubClass.value); } }
輸出結(jié)果是:super class init。
/** * 被動引用情景2 * 通過數(shù)組引用來引用類,不會觸發(fā)此類的初始化 * @author volador * */ public class test{ public static void main(String[] args){ SuperClass s_list=new SuperClass[10]; } }
輸出結(jié)果:沒輸出
/** * 被動引用情景3 * 常量在編譯階段會被存入調(diào)用類的常量池中,本質(zhì)上并沒有引用到定義常量類類,所以自然不會觸發(fā)定義常量的類的初始化 * @author root * */ class ConstClass{ static{ System.out.println("ConstClass init."); } public final static String value="hello"; } public class test{ public static void main(String[] args){ System.out.println(ConstClass.value); } }
輸出結(jié)果:hello(tip:在編譯的時候,ConstClass.value已經(jīng)被轉(zhuǎn)變成hello常量放進(jìn)test類的常量池里面了)
以上是針對類的初始化,接口也要初始化,接口的初始化跟類的初始化有點不同:
上面的代碼都是用static{}來輸出初始化信息的,接口沒法做到,但接口初始化的時候編譯器仍然會給接口生成一個<clinit>()的類構(gòu)造器,用來初始化接口中的成員變量,這點在類的初始化上也有做到。真正不同的地方在于第三點,類的初始化執(zhí)行之前要求父類全部都初始化完成了,但接口的初始化貌似對父接口的初始化不怎么感冒,也就是說,子接口初始化的時候并不要求其父接口也完成初始化,只有在真正使用到父接口的時候它才會被初始化(比如引用接口上的常量的時候啦)。
下面分解一下一個類的加載全過程:加載->驗證->準(zhǔn)備->解析->初始化
首先是加載:
這一塊虛擬機要完成3件事:
1.通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
3.在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
關(guān)于第一點,很靈活,很多技術(shù)都是在這里切入,因為它并沒有限定二進(jìn)制流從哪里來:
從class文件來->一般的文件加載
從zip包中來->加載jar中的類
從網(wǎng)絡(luò)中來->Applet
..........
相比與加載過程的其他幾個階段,加載階段可控性最強,因為類的加載器可以用系統(tǒng)的,也可以用自己寫的,程序猿可以用自己的方式寫加載器來控制字節(jié)流的獲取。
獲取二進(jìn)制流獲取完成后會按照jvm所需的方式保存在方法區(qū)中,同時會在java堆中實例化一個java.lang.Class對象與堆中的數(shù)據(jù)關(guān)聯(lián)起來。
加載完成后就要開始對那些字節(jié)流進(jìn)行檢驗了(其實很多步驟是跟上面交叉進(jìn)行的,比如文件格式驗證):
檢驗的目的:確保class文件的字節(jié)流信息符合jvm的口味,不會讓jvm感到不舒服。假如class文件是由純粹的java代碼編譯過來的,自然不會出現(xiàn)類似于數(shù)組越界、跳轉(zhuǎn)到不存在的代碼塊等不健康的問題,因為一旦出現(xiàn)這種現(xiàn)象,編譯器就會拒絕編譯了。但是,跟之前說的一樣,Class文件流不一定是從java源碼編譯過來的,也可能是從網(wǎng)絡(luò)或者其他地方過來的,甚至你可以自己用16進(jìn)制寫,假如jvm不對這些數(shù)據(jù)進(jìn)行校驗的話,可能一些有害的字節(jié)流會讓jvm完全崩潰。
檢驗主要經(jīng)歷幾個步驟:文件格式驗證->元數(shù)據(jù)驗證->字節(jié)碼驗證->符號引用驗證
文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范并 驗證其版本是否能被當(dāng)前的jvm版本所處理。ok沒問題后,字節(jié)流就可以進(jìn)入內(nèi)存的方法區(qū)進(jìn)行保存了。后面的3個校驗都是在方法區(qū)進(jìn)行的。
元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進(jìn)行語義化分析,保證其描述的內(nèi)容符合java語言的語法規(guī)范。
字節(jié)碼檢驗:最復(fù)雜,對方法體的內(nèi)容進(jìn)行檢驗,保證其在運行時不會作出什么出格的事來。
符號引用驗證:來驗證一些引用的真實性與可行性,比如代碼里面引了其他類,這里就要去檢測一下那些來究竟是否存在;或者說代碼中訪問了其他類的一些屬性,這里就對那些屬性的可以訪問行進(jìn)行了檢驗。(這一步將為后面的解析工作打下基礎(chǔ))
驗證階段很重要,但也不是必要的,假如說一些代碼被反復(fù)使用并驗證過可靠性了,實施階段就可以嘗試用-Xverify:none參數(shù)來關(guān)閉大部分的類驗證措施,以簡短類加載時間。
接著就上面步驟完成后,就會進(jìn)入準(zhǔn)備階段了:
這階段會為類變量(指那些靜態(tài)變量)分配內(nèi)存并設(shè)置類比那輛初始值的階段,這些內(nèi)存在方法區(qū)中進(jìn)行分配。這里要說明一下,這一步只會給那些靜態(tài)變量設(shè)置一個初始的值,而那些實例變量是在實例化對象時進(jìn)行分配的。這里的給類變量設(shè)初始值跟類變量的賦值有點不同,比如下面:
public static int value=123;
在這一階段,value的值將會是0,而不是123,因為這個時候還沒開始執(zhí)行任何java代碼,123還是不可見的,而我們所看到的把123賦值給value的putstatic指令是程序被編譯后存在于<clinit>(),所以,給value賦值為123是在初始化的時候才會執(zhí)行的。
這里也有個例外:
public static final int value=123;
這里在準(zhǔn)備階段value的值就會初始化為123了。這個是說,在編譯期,javac會為這個特殊的value生成一個ConstantValue屬性,并在準(zhǔn)備階段jm就會根據(jù)這個ConstantValue的值來為value賦值了。
完成上步后,就要進(jìn)行解析了。解析好像是對類的字段,方法等東西進(jìn)行轉(zhuǎn)換,具體涉及到Class文件的格式內(nèi)容,并沒深入去了解。
初始化過程是類加載過程的最后一步:
在前面的類加載過程中,除了在加載階段用戶可以通過自定義類加載器參與之外,其他的動作完全有jvm主導(dǎo),到了初始化這塊,才開始真正執(zhí)行java里面的代碼。
這一步將會執(zhí)行一些預(yù)操作,注意區(qū)分在準(zhǔn)備階段,已經(jīng)為類變量執(zhí)行過一次系統(tǒng)賦值了。
其實說白了,這一步就是執(zhí)行程序的<clinit>();方法的過程。下面我們來研究一下<clinit>()方法:
<clinit>()方法叫做類構(gòu)造器方法,有編譯器自動手機類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并而成的,置于他們的順序與在源文件中排列的一樣。
<clinit>();方法與類構(gòu)造方法不一樣,他不需要顯示得調(diào)用父類的<clinit>();方法,虛擬機會保證子類的<clinit>();方法在執(zhí)行前父類的這個方法已經(jīng)執(zhí)行完畢了,也就是說,虛擬機中第一個被執(zhí)行的<clinit>();方法肯定是java.lang.Object類的。
下面來個例子說明一下:
static class Parent{ public static int A=1; static{ A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.B); }
首先Sub.B中對靜態(tài)數(shù)據(jù)進(jìn)行了引用,Sub類要進(jìn)行初始化了。同時,其父類Parent要先進(jìn)行初始化動作。Parent初始化后,A=2,所以B=2;上個過程相當(dāng)于:
static class Parent{ <clinit>(){ public static int A=1; static{ A=2; } } } static class Sub extends Parent{ <clinit>(){ //jvm會先讓父類的該方法執(zhí)行完在執(zhí)行這里 public static int B=A; } } public static void main(String[] args){ System.out.println(Sub.B); }
<clinit>();方法對類跟接口來說不是必須的,加入類或者接口中沒有對類變量進(jìn)行賦值且沒有靜態(tài)代碼塊,<clinit>()方法就不會被編譯器生成。
由于接口里面不能存在static{}這種靜態(tài)代碼塊,但仍然可能存在變量初始化時的變量賦值操作,所以接口里面也會生成<clinit>()構(gòu)造器。但跟類的不同的是,執(zhí)行子接口的<clinit>();方法前并不需要執(zhí)行父接口的<clinit>();方法,當(dāng)父接口中定義的變量被使用時,父接口才會被初始化。
另外,接口的實現(xiàn)類在初始化的時候也一樣不會執(zhí)行接口的<clinit>();方法。
另外,jvm會保證一個類的<clinit>();方法在多線程環(huán)境下能被正確地加鎖同步。<因為初始化只會被執(zhí)行一次>。
下面用個例子說明一下:
public class DeadLoopClass { static{ if(true){ System.out.println("要被 ["+Thread.currentThread()+"] 初始化了,下面來一個無限循環(huán)"); while(treu){} } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("toplaile"); Runnable run=new Runnable(){ @Override public void run() { // TODO Auto-generated method stub System.out.println("["+Thread.currentThread()+"] 要去實例化那個類了"); DeadLoopClass d=new DeadLoopClass(); System.out.println("["+Thread.currentThread()+"] 完成了那個類的初始化工作"); }}; new Thread(run).start(); new Thread(run).start(); } }
關(guān)于Java中是如何加載類的問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。