在Java語言里雇盖,類的加載、連接和初始化都是在程序運行期間完成的栖忠。這種方式雖然在性能上會一定的開銷崔挖,但是它會是Java的應(yīng)用程序具有更高的靈活性。
比如:
- 我們在寫一個類中用了一個接口的方法庵寞,現(xiàn)在這個類編譯后的class中是不知道我們具體是哪個類實現(xiàn)的狸相,只有等到了運行程序的時候才指定了具體的實現(xiàn)類。
- 另外我們可以通過先定義類加載器捐川,然后我們可以隨時從任何地方加載一個二進制流來動態(tài)的加載一個類脓鹃。
- 可以實現(xiàn)動態(tài)的替換jsp,還有OSGI的熱插拔技術(shù)
這些都是java提供給我們的可以用類加載機制來實現(xiàn)的功能
Java中類的生命周期
加載(Loading)古沥、驗證(Verification)将谊、準(zhǔn)備(Preparation)、解析(Resolution)渐白、初始化(Initialization)尊浓、使用(Using)、卸載(Unloading)
一個類的存在的順序大致會按照這個順序進行纯衍,但是也會存在特殊的情況栋齿,在初始化的時候去解析
Java中什么情況下需要對類進行初始化(階段),再此之前其他的操作:加載襟诸、驗證等已經(jīng)完成
- 在使用new創(chuàng)建個對象時瓦堵,或者使用這個類的靜態(tài)方法或者靜態(tài)變量(有一種情況除外,在靜態(tài)變量被final修飾的時候不會初始化對象歌亲,因為這種對象在編譯期間已經(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)運行到的時候在去滤港,如果發(fā)現(xiàn)沒有初始化的話再去進行初始化這個類)
加載階段
加載階段虛擬機主要做了這三件事情:
- 通過一個類的全限定名(包名+類名)來獲取定義此類的二進制流(得到Class二進制流)
- 把這個字節(jié)流鎖代表的經(jīng)他愛存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)(把流轉(zhuǎn)化成方法區(qū)里能夠使用的數(shù)據(jù)結(jié)構(gòu))
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口(生成Class對象趴拧,注:Class對象比較特殊溅漾,在HotSpot虛擬機中它是在方法區(qū)內(nèi)不是在堆中)
這三個階段中可控性最強的就是第一個得到二進制流的階段山叮,我們可以通過各種方式來得到,比如從本地磁盤添履,從數(shù)據(jù)庫屁倔,從網(wǎng)絡(luò)上...等到流之后我們可以重寫一個類的加載器的loadClass()方法,或者是使用JDK提供的引導(dǎo)類加載器來完成缝龄。
對于數(shù)組和其他引用類型來說有些區(qū)別汰现,數(shù)組本身是不通過類加載器創(chuàng)建的,它是由Java虛擬機自己直接創(chuàng)建的叔壤。但是數(shù)組類與類加載器還是有很多關(guān)系的瞎饲,具體如下:
- 如果數(shù)組的組件類型是引用類型(就是數(shù)組去掉第一個維度的類型),那么就是使用對應(yīng)的加載器加載組件類型炼绘,然后數(shù)組將會被組件類型的類加載器上被標(biāo)識嗅战。
- 如果數(shù)組的組件類型不是引用類型(比如int等基本數(shù)據(jù)類型),java虛擬機將會吧數(shù)組C標(biāo)記與引導(dǎo)類加載器關(guān)聯(lián)
- 數(shù)組類的可見性與它的組件類型的可見性是一致的俺亮,如果組件類型不是引用類型驮捍,那么這個數(shù)組類的可見性就是public(就是數(shù)組這個類的可見性和它里面的類的可見性是一致的)
驗證階段
驗證階段是為了確保Class文件的字節(jié)流中包含的信息是符合房錢虛擬機要求的。
雖然Java本身是相對安全的脚曾,因為它有編譯成Class這一步倒源。編譯器是有一定規(guī)則的查吊,如果你寫的不符合要求會拒絕編譯。但我們知道Class文件并不是只靠Java源碼編譯過來的,它可以是通過任何途徑獲得(如果你自己用十六進制編輯器寫了一個十六進制文件秘遏,只要符合要求也是可以運行的)胯府。如果虛擬機沒有檢查輸入的字節(jié)流那么可能會導(dǎo)致很嚴(yán)重的后果延届。(可能會有惡意的代碼消玄,或者不是惡意的但會造成程序的崩潰)
大致上驗證階段可以分為以下4個階段:
- 文件格式驗證
- 元數(shù)據(jù)驗證
- 字節(jié)碼驗證
- 符號引用驗證
準(zhǔn)備階段
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些都會在方法區(qū)內(nèi)分配撞芍。(這里的分配變量知識分配類變量秧了,就是用static修飾的變量,不包括手里邊了序无,實例變量將會在對象實例化的時候進行賦值验毡。)
下圖是基本數(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)的指針动看、偏移量或者是句柄。直接引用和虛擬機內(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)造器,因為虛擬機會保證在子類執(zhí)行之前父類的<clinit>方法一定會執(zhí)行成功钉赁。因此我們可以知道在虛擬機中第一個被執(zhí)行的<clinit>方法一定是java.lang.Object
接口中是不能使用靜態(tài)語句塊的蹄殃,但是可以有靜態(tài)變量賦值的操作。因此也會生成<clinit>方法你踩。但接口月累不同的是執(zhí)行接口不需要先執(zhí)行父接口的方法诅岩,只有當(dāng)付借款使用時才會初始化。
虛擬機會保證一個類的<clinit>方法會被正確的加鎖姓蜂,也就是在多個線程初始化一個類時只會有一個線程去執(zhí)行這個類的方法按厘。注意這種可能會造成阻塞(其他線程不會在重新執(zhí)行一次,一個類只會執(zhí)行一次)