基于JVM的語(yǔ)言,如java,kotlin,groovy等語(yǔ)言,在各自編譯器編譯完成之后,都會(huì)編譯為.class文件,用JVM加載。而class文件只有被確的加載到JVM正中才能運(yùn)行和使用。虛擬機(jī)是如何在家這些文件呢?本文將詳細(xì)講解。
類的生命周期
一個(gè)類從被加載到虛擬機(jī)到最后被卸載,生命周期包括:加載,驗(yàn)證,準(zhǔn)備,解析,初始化,使用,卸載7個(gè)階段。其中驗(yàn)證,準(zhǔn)備,解析3個(gè)部分稱為連接階段。

這7個(gè)階段在實(shí)際JVM中并不是按照?qǐng)D中所示的順序來(lái)開始運(yùn)行的,里面存在時(shí)間上的交叉進(jìn)行。但是其中加載,驗(yàn)證,準(zhǔn)備,初始化,卸載5個(gè)結(jié)算的順序是確定的。
加載
這是類生命周期的第一個(gè)階段,那么加載的是什么呢?加載的應(yīng)該是一個(gè)字節(jié)碼文件的二進(jìn)制字節(jié)流。那么此二進(jìn)制流如何得來(lái)呢?java虛擬機(jī)規(guī)范并沒(méi)有強(qiáng)制要求,我們可以靈活運(yùn)用這一特性實(shí)現(xiàn)很多的加載源:
- 最常見(jiàn)的,從壓縮包獲取,比如jar,EAR,WAR等
- 從網(wǎng)絡(luò)中獲取,比如早期嵌入在瀏覽器中的Applet程序
- 在運(yùn)行是生成字節(jié)碼,動(dòng)態(tài)代理技術(shù)。如著名的
GCLib字節(jié)碼類庫(kù),再如現(xiàn)在Android中常用的網(wǎng)絡(luò)請(qǐng)求庫(kù)retrofit中所使用的動(dòng)態(tài)代理Proxy.newProxyInstance()中,最終會(huì)調(diào)用的sun.misc.ProxyGenerator.generateProxyClass()方法,該方法在運(yùn)行時(shí)動(dòng)態(tài)產(chǎn)生了一組字節(jié)碼流(標(biāo)識(shí)為$Proxy的代理類)。 - 由其他文件生成,比如由JSP文件生成class類
- 等等等等
在此階段,開發(fā)人員可以使用系統(tǒng)的類加載器進(jìn)行加載,也可以使用自己定義的類加載器來(lái)自定義獲取字節(jié)碼流的方式(重寫來(lái)加載器的loadClass方法)。
加載字節(jié)碼文件結(jié)束后,虛擬機(jī)將字節(jié)流存儲(chǔ)在方法區(qū)中,同時(shí)在內(nèi)存中(Hot Spot中實(shí)在方法區(qū)中)實(shí)例化一個(gè)Class對(duì)象,外部可以同過(guò)此實(shí)例訪問(wèn)該類對(duì)象。
在此階段運(yùn)行中,驗(yàn)證階段就已開始,交叉進(jìn)行。只有通過(guò)通過(guò)了驗(yàn)證階段,只有通過(guò)了驗(yàn)證階段,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)。
驗(yàn)證
驗(yàn)證階段的主要任務(wù)是:確保字節(jié)碼流中包含的信息符合當(dāng)前版本虛擬機(jī)的要求,并不會(huì)有危害虛擬機(jī)自身安全的行為。
如:將一個(gè)對(duì)象強(qiáng)轉(zhuǎn)為一個(gè)未聲明實(shí)現(xiàn)的類型,執(zhí)行一個(gè)虛方法,執(zhí)行一個(gè)并不存在的方法。在我們平時(shí)編碼的經(jīng)驗(yàn)中,雖然以上這些錯(cuò)誤會(huì)在編譯時(shí)報(bào)出,無(wú)法通過(guò)編譯;但是,我們上面提到過(guò),class文件是由多種方式得來(lái),對(duì)于直接生成.class文件、無(wú)需編譯的方式,驗(yàn)證這一階段對(duì)于虛擬機(jī)的保護(hù)就顯得尤其重要。
簡(jiǎn)要的概述,虛擬機(jī)對(duì)類的驗(yàn)證階段分為以下4個(gè)方面,這四個(gè)方面層層深入:
文件格式的驗(yàn)證
針對(duì)類文件(字節(jié)碼流)的驗(yàn)證
驗(yàn)證字節(jié)碼流是否符合java虛擬機(jī)規(guī)范中規(guī)定的class文件格式,如:
- 魔數(shù)是否為CAFEBABY
- 當(dāng)前虛擬機(jī)持否可以處理文件聲明的主,次版本號(hào)
- 常量池中是否有不被支持的常量類型
- 檢查指向常量的索引是否指向了不存在的常量
- CONSTANT_Utf8_info型的常量是否符合Utf8編碼
- Class文件中的各個(gè)部分是否被刪除(class文件是否完整)
- 等等
通過(guò)了驗(yàn)證階段,字節(jié)流會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)。以后的驗(yàn)證和其他操作都針對(duì)于內(nèi)存方法區(qū)中的數(shù)據(jù)進(jìn)行操作,而不針對(duì)字節(jié)碼流。
元數(shù)據(jù)驗(yàn)證
針對(duì)數(shù)據(jù)類型的驗(yàn)證
該階段是進(jìn)行語(yǔ)義分析驗(yàn)證,以保證其信息符合Java語(yǔ)言規(guī)范的要求,比如:
- 檢查這個(gè)類是否有父類(除了Object之外都應(yīng)有父類)
- 本類的父類是否繼承了不允許被繼承的類(被final修飾)
- 如果本類不是抽象類,是否實(shí)現(xiàn)了父類中的全部虛方法或接口
- 等等
字節(jié)碼驗(yàn)證
針對(duì)方法體的驗(yàn)證
此階段通過(guò)數(shù)據(jù)流和控制流分析,檢查程序的語(yǔ)義是合法的,符合邏輯的。保證程序邏輯的正確運(yùn)行,檢驗(yàn)的內(nèi)容如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作。(如:不能出現(xiàn)這樣的狀況:操作棧中放了一個(gè)int類型的數(shù)據(jù),使用卻按照l(shuí)ong或者引用類型加載)
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 類型轉(zhuǎn)換是有效的(如多態(tài))
- 等等
符號(hào)引用的驗(yàn)證
針對(duì)常量池匹配的驗(yàn)證
此階段檢查是為了:確保在后續(xù)的解析階段,虛擬機(jī)可以順利的將符號(hào)引用轉(zhuǎn)化為直接引用。(關(guān)于符號(hào)引用與直接引用的概念,祥見(jiàn)下文解析過(guò)程)見(jiàn)下圖,講解一下驗(yàn)證內(nèi)容:

- 符號(hào)引用中通過(guò)字符串的描述能夠找對(duì)應(yīng)的類(如:上圖中常量池中有一個(gè)指向類型為class的常量
#4 = Class #17 // java/lang/Object,應(yīng)該確保有一個(gè)類與之對(duì)應(yīng),此處為String類) - 符號(hào)引用中通過(guò)字符串的描述的能夠找到相應(yīng)的方法(如:上圖中
#2 = Methodref #3.#15 // VinctorTest.test:()V描述的,需要在VinctorTest類中有一個(gè)test方法與之對(duì)應(yīng)) - 符號(hào)引用中的用到的類,方法,字段的訪問(wèn)性(public private等)確??梢员划?dāng)前類訪問(wèn)到
- 等等
準(zhǔn)備
針對(duì)類變量(static)
經(jīng)過(guò)驗(yàn)證階段,虛擬機(jī)從文件,數(shù)據(jù)類型,方法邏輯,符號(hào)引用等各個(gè)方面對(duì)類進(jìn)行了驗(yàn)證,已確保代碼的正確性。接下來(lái)開始為代碼的運(yùn)行做準(zhǔn)備,進(jìn)入準(zhǔn)備階段。
準(zhǔn)備階段是為正式類變量(注意,不是實(shí)例變量)分配內(nèi)存并設(shè)置類變量初始值的階段,注意,此初始值并不是我們java代碼中所寫的初始值(如 int a=123;),而是java虛擬機(jī)規(guī)范中規(guī)定的初始值,
java體系中各種類型的初始值如下:

如果一個(gè)變量聲明為static int a=123,則在此階段,聲明a的值為0;
注意:如果類變量被final修飾,如
final static int a=123;
這種情況下,javac編譯階段,將為此變量生成ConstantValue屬性,在此準(zhǔn)備階段直接將其賦值為123;
解析
針對(duì)常量池
解析階段是將常量池中符號(hào)引用轉(zhuǎn)化成直接引用的過(guò)程。主要針對(duì)常量池中的類或接口,字段,類方法,接口方法,方法類型,方法句柄,調(diào)用限定符
-
符號(hào)引用:見(jiàn)上文中class文件中常量池的圖片,我們可以知道常量池中有描述類,方法,字段等常量,這些常量通過(guò)一組符號(hào)(比如UTF8字符串)描述所引用的目標(biāo)。雖然在驗(yàn)證階段已經(jīng)對(duì)此進(jìn)行了驗(yàn)證,但是這些畢竟只是一些字符串,并不能拿來(lái)直接為虛擬機(jī)使用,并不指向任何真實(shí)的內(nèi)存地址。 -
直接引用:直接引用則是指向這些目標(biāo)的指針,偏移量或者句柄。
直接引用指向的目標(biāo)必須真實(shí)存在于內(nèi)存之中的。在代碼運(yùn)行過(guò)程中,會(huì)不斷產(chǎn)生新對(duì)象,故而解析這一過(guò)程并不是一次就完成的,其發(fā)生的時(shí)機(jī)不固定。
java虛擬機(jī)規(guī)范中規(guī)定了只有執(zhí)行了以下字節(jié)碼指令前才會(huì)將所用到的符號(hào)引用轉(zhuǎn)化為直接引用:
-
anewarray創(chuàng)建一個(gè)引用類型的數(shù)組 -
checkcast檢查對(duì)象是否是給定類型 -
getfieldputfield從對(duì)象獲取某一個(gè)字段 設(shè)置對(duì)象的字段 -
getstaticputstatic從類中獲取某一靜態(tài)變量 設(shè)置靜態(tài)變量 -
instanceof確定對(duì)象是否是給定類型 -
invokedynamicinvokeinterfaceinvokestaticinvokevirtual調(diào)用動(dòng)態(tài)方法,接口方法,靜態(tài)方法,虛方法 -
invokespecial調(diào)用實(shí)例化方法,私有方法,父類中的方法 -
ldcidc_w把常量池中的項(xiàng)壓入棧 -
multianewarray創(chuàng)建多為引用類型性數(shù)組 -
new實(shí)例化對(duì)象
在解析過(guò)程中,如果需要解析類或接口的的字段,方法,則先查找該字段,方法所屬的類或接口是否被解析,如果沒(méi)有,則先解析類或接口,然后在查找當(dāng)前的類或接口中是否有該字段或方法,如果沒(méi)有,則遞歸向上到父類或父接口中尋找該字段或接口。
初始化
至此,程序終于開始執(zhí)行我們開發(fā)人員寫的代碼了(等了好久)。此階段是為類設(shè)置類變量的值和一些其他初始化操作的階段(如執(zhí)行static{ }靜態(tài)代碼塊)。
在類編譯過(guò)充中,編譯器為每一個(gè)方法生成了一個(gè)<clinit>()類初始化方法,初始化階段也是此方法的執(zhí)行階段。
注意
<clinit>()并不是默認(rèn)構(gòu)造方法,前者是類的初始化方法,后者是實(shí)例的初始化方法。我們此文討論的是類的生命周期,而不是實(shí)例的生命周期。
<clinit>()是如何生成的呢?其中又包含什么呢?
<clinit>()方法是在編譯階段,編譯器收集整個(gè)類中的類變量的賦值以及靜態(tài)代碼塊而形成的。順序是按照賦值以及靜態(tài)代碼在源文件中出現(xiàn)的順序生成的。同時(shí),如果一個(gè)類有父類,則虛擬機(jī)會(huì)保證父類的初始化先于子類的初始化執(zhí)行。
使用
至此 一個(gè)類已經(jīng)具備我們使用的條件了,我們可以對(duì)這個(gè)類進(jìn)行實(shí)例化和其他操作了。
github上的地址:DevelopBlog