在Java語言里,類的加載、連接和初始化都是在程序運(yùn)行期間完成的。這種方式雖然在性能上會一定的開銷,但是它會是Java的應(yīng)用程序具有更高的靈活性。
比如:
- 我們在寫一個類中用了一個接口的方法,現(xiàn)在這個類編譯后的class中是不知道我們具體是哪個類實(shí)現(xiàn)的,只有等到了運(yùn)行程序的時候才指定了具體的實(shí)現(xiàn)類。
- 另外我們可以通過先定義類加載器,然后我們可以隨時從任何地方加載一個二進(jìn)制流來動態(tài)的加載一個類。
- 可以實(shí)現(xiàn)動態(tài)的替換jsp,還有OSGI的熱插拔技術(shù)
這些都是java提供給我們的可以用類加載機(jī)制來實(shí)現(xiàn)的功能
Java中類的生命周期
加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)
一個類的存在的順序大致會按照這個順序進(jìn)行,但是也會存在特殊的情況,在初始化的時候去解析
Java中什么情況下需要對類進(jìn)行初始化(階段),再此之前其他的操作:加載、驗(yàn)證等已經(jīng)完成
- 在使用new創(chuàng)建個對象時,或者使用這個類的靜態(tài)方法或者靜態(tài)變量(有一種情況除外,在靜態(tài)變量被final修飾的時候不會初始化對象,因?yàn)檫@種對象在編譯期間已經(jīng)把變量放在了常量池中了。)
- 在使用java.lang.reflect包中的方法,也就是在使用反射的時候會觸發(fā)初始化操作
- 初始化一個類的時候如果還沒有初始化父類的時候會初始化父類
- 啟動的時候會初始化執(zhí)行類的主類(Main方法)
- JDK1.7加上了動態(tài)語言支持,就是在遇到REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄時這個類如果沒有初始化才會觸發(fā)其初始化。(就是類似于Js、Python動態(tài)語言,不需要事先確定這個參數(shù)的類型,當(dāng)運(yùn)行到的時候在去,如果發(fā)現(xiàn)沒有初始化的話再去進(jìn)行初始化這個類)
加載階段
加載階段虛擬機(jī)主要做了這三件事情:
- 通過一個類的全限定名(包名+類名)來獲取定義此類的二進(jìn)制流(得到Class二進(jìn)制流)
- 把這個字節(jié)流鎖代表的經(jīng)他愛存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)(把流轉(zhuǎn)化成方法區(qū)里能夠使用的數(shù)據(jù)結(jié)構(gòu))
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口(生成Class對象,注:Class對象比較特殊,在HotSpot虛擬機(jī)中它是在方法區(qū)內(nèi)不是在堆中)
這三個階段中可控性最強(qiáng)的就是第一個得到二進(jìn)制流的階段,我們可以通過各種方式來得到,比如從本地磁盤,從數(shù)據(jù)庫,從網(wǎng)絡(luò)上...等到流之后我們可以重寫一個類的加載器的loadClass()方法,或者是使用JDK提供的引導(dǎo)類加載器來完成。
對于數(shù)組和其他引用類型來說有些區(qū)別,數(shù)組本身是不通過類加載器創(chuàng)建的,它是由Java虛擬機(jī)自己直接創(chuàng)建的。但是數(shù)組類與類加載器還是有很多關(guān)系的,具體如下:
- 如果數(shù)組的組件類型是引用類型(就是數(shù)組去掉第一個維度的類型),那么就是使用對應(yīng)的加載器加載組件類型,然后數(shù)組將會被組件類型的類加載器上被標(biāo)識。
- 如果數(shù)組的組件類型不是引用類型(比如int等基本數(shù)據(jù)類型),java虛擬機(jī)將會吧數(shù)組C標(biāo)記與引導(dǎo)類加載器關(guān)聯(lián)
- 數(shù)組類的可見性與它的組件類型的可見性是一致的,如果組件類型不是引用類型,那么這個數(shù)組類的可見性就是public(就是數(shù)組這個類的可見性和它里面的類的可見性是一致的)
驗(yàn)證階段
驗(yàn)證階段是為了確保Class文件的字節(jié)流中包含的信息是符合房錢虛擬機(jī)要求的。
雖然Java本身是相對安全的,因?yàn)樗芯幾g成Class這一步。編譯器是有一定規(guī)則的,如果你寫的不符合要求會拒絕編譯。但我們知道Class文件并不是只靠Java源碼編譯過來的,它可以是通過任何途徑獲得(如果你自己用十六進(jìn)制編輯器寫了一個十六進(jìn)制文件,只要符合要求也是可以運(yùn)行的)。如果虛擬機(jī)沒有檢查輸入的字節(jié)流那么可能會導(dǎo)致很嚴(yán)重的后果。(可能會有惡意的代碼,或者不是惡意的但會造成程序的崩潰)
大致上驗(yàn)證階段可以分為以下4個階段:
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證
- 字節(jié)碼驗(yàn)證
- 符號引用驗(yàn)證
準(zhǔn)備階段
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些都會在方法區(qū)內(nèi)分配。(這里的分配變量知識分配類變量,就是用static修飾的變量,不包括手里邊了,實(shí)例變量將會在對象實(shí)例化的時候進(jìn)行賦值。)
下圖是基本數(shù)據(jù)類型的初始值,引用類型的初始值是null
注意:上面說的是通常的情況,有一種情況是比較特殊的。就是在用final修飾了之后會在初始化階段直接初始化上ConstantValue的值。例如:
public static final int value =123;
這時候在編譯階段會吧value的值附上123了,準(zhǔn)備階段就自然會給value賦值成123
解析階段
在解析階段是把常量池中的符號引用轉(zhuǎn)化成直接引用,符號引用是在編譯成Class文件的時候生成的。
直接引用和符號引用之間的定義:
- 符號引用:符號引用用一組符號聊描述所引用的目標(biāo),符號只需要沒有重復(fù)的能定位到目標(biāo)即可。這個目標(biāo)是不一定會加載到內(nèi)存中的,也就是說這時候目標(biāo)還不存在。
- 直接引用:直接引用是指向目標(biāo)的指針、偏移量或者是句柄。直接引用和虛擬機(jī)內(nèi)存布局有關(guān)系。也就是說如果有了直接引用就說明這個目標(biāo)已經(jīng)在內(nèi)存存在了。
初始化階段
初始化階段必須是在前面的步驟都結(jié)束后才執(zhí)行的最后一步。初始化階段會真正的執(zhí)行類中定義的java程序代碼。
在初始化階段是執(zhí)行類構(gòu)造器方法的過程(就是執(zhí)行<clinit>類型的方法)。
- <clinit>方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中語句合并產(chǎn)生的結(jié)果(static{})。這個執(zhí)行順序是由語句在源文件出現(xiàn)的順序所決定的。也就是說靜態(tài)語句塊只能訪問到定義在靜態(tài)語句塊之前的變量,但是定義在后邊的變量可以在前面的靜態(tài)語句塊中被復(fù)制。但是不能訪問
public class Test{
static{
i =0;
System.out.print(i); //這塊會有編譯錯誤非法向前引用
}
static int i = 1;
}
<clinit>方法與類的構(gòu)造函數(shù)不同,它不需要顯示的調(diào)用父類的構(gòu)造器,因?yàn)樘摂M機(jī)會保證在子類執(zhí)行之前父類的<clinit>方法一定會執(zhí)行成功。因此我們可以知道在虛擬機(jī)中第一個被執(zhí)行的<clinit>方法一定是java.lang.Object
接口中是不能使用靜態(tài)語句塊的,但是可以有靜態(tài)變量賦值的操作。因此也會生成<clinit>方法。但接口月累不同的是執(zhí)行接口不需要先執(zhí)行父接口的方法,只有當(dāng)付借款使用時才會初始化。
虛擬機(jī)會保證一個類的<clinit>方法會被正確的加鎖,也就是在多個線程初始化一個類時只會有一個線程去執(zhí)行這個類的方法。注意這種可能會造成阻塞(其他線程不會在重新執(zhí)行一次,一個類只會執(zhí)行一次)