前言
一個(gè)Java類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,它經(jīng)過了哪些步驟呢?這篇文章就來簡述一下關(guān)于Java類生命周期相關(guān)的知識,其中每個(gè)生命周期的具體內(nèi)容不會(huì)細(xì)講,因?yàn)閮?nèi)容太多,我準(zhǔn)備專門花一篇文章介紹類生命周期中的詳細(xì)步驟。
概述
一個(gè)Java類從開始到結(jié)束整個(gè)生命周期會(huì)經(jīng)歷7個(gè)階段:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)。其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分又統(tǒng)稱為連接(Linking)。
這里我所說的Java類是已經(jīng)編譯好的類,也就是說它已經(jīng)是class字節(jié)碼了,如果要從.java文件算起的話應(yīng)該還有個(gè)編譯過程。
每個(gè)階段的順序
并不是所有時(shí)候這七個(gè)階段都是順序進(jìn)行的,其中加載、驗(yàn)證、準(zhǔn)備、初始化、卸載是固定順序開始的,解析階段不一定。解析在某些情況下可以在初始化階段之后再開始,這也是為了支持運(yùn)行時(shí)綁定(也成為動(dòng)態(tài)綁定)。剛剛說的五個(gè)階段是固定順序開始,但是不一定會(huì)按部就班地“進(jìn)行”或“完成”,是因?yàn)檫@些階段通常是互相交叉地混合進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用激活另一個(gè)階段。
簡述七個(gè)階段
這里先簡單介紹一下各個(gè)階段所做的事,每個(gè)階段詳細(xì)的過程在后面會(huì)有專門的文章介紹。
加載:加載過程就是把class字節(jié)碼文件載入到虛擬機(jī)中,至于從哪兒加載,虛擬機(jī)設(shè)計(jì)者并沒有限定,你可以從文件、壓縮包、網(wǎng)絡(luò)、數(shù)據(jù)庫等等地方加載class字節(jié)碼。
通過類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
將此二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生成代表此類的java.lang.Class對象,作為該類訪問入口.
驗(yàn)證:驗(yàn)證的目的是確保class文件的字節(jié)流中信息符合虛擬機(jī)的要求,不會(huì)危害虛擬機(jī)安全,使得虛擬機(jī)免受惡意代碼的攻擊,這一步至關(guān)重要。
文件格式驗(yàn)證
源數(shù)據(jù)驗(yàn)證
字節(jié)碼驗(yàn)證
符號引用驗(yàn)證
準(zhǔn)備:準(zhǔn)備階段的工作就是為類的靜態(tài)變量分配內(nèi)存并設(shè)為jvm默認(rèn)的初值,對于非靜態(tài)的變量,則不會(huì)為它們分配內(nèi)存。靜態(tài)變量的初值為jvm默認(rèn)的初值,而不是我們在程序中設(shè)定的初值。(僅包含類變量,不包含實(shí)例變量).
解析:虛擬機(jī)將常量池中的符號引用替換為直接引用,解析動(dòng)作主要針對類或接口,字段,類方法,方法類型等等。
初始化:在該階段,才真正意義上的開始執(zhí)行類中定義的java程序代碼,該階段會(huì)執(zhí)行類構(gòu)造器,并且在Java虛擬機(jī)規(guī)范中有明確的規(guī)定,在下面5種情況下必須對類進(jìn)行初始化:
遇到new、getstatic、putstatic、invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
使用java.long.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
當(dāng)虛擬機(jī)啟動(dòng)時(shí),需要制定一個(gè)執(zhí)行的主類(即main方法的類),虛擬機(jī)必須先初始化這個(gè)類。
使用動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后解析結(jié)果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄對應(yīng)的類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化。
使用:使用該類所提供的功能,其中包括主動(dòng)引用和被動(dòng)引用。
主動(dòng)引用:
通過new關(guān)鍵字實(shí)例化對象、讀取或設(shè)置類的靜態(tài)變量、調(diào)用類的靜態(tài)方法。
通過反射方式執(zhí)行以上三種行為。
初始化子類的時(shí)候,會(huì)觸發(fā)父類的初始化。
作為程序入口直接運(yùn)行時(shí)(也就是直接調(diào)用main方法)。
被動(dòng)引用:
引用父類的靜態(tài)字段,只會(huì)引起父類的初始化,而不會(huì)引起子類的初始化。
定義類數(shù)組,不會(huì)引起類的初始化。
引用類的常量,不會(huì)引起類的初始化。
卸載:從內(nèi)存中釋放,在我之前寫的垃圾回收機(jī)制(GC)總結(jié)一文中有介紹到方法區(qū)內(nèi)存回收中對類的回收條件,這里再貼出來一下:
該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例;
加載該類的ClassLoader已經(jīng)被回收;
該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
引用類時(shí)容易忽略的點(diǎn)
在平時(shí)做面試題中很有可能會(huì)考察對類加載流程的理解,有的是直接給你幾個(gè)描述讓你選擇,有的是給出一段代碼,讓你判斷輸出結(jié)果。第一種方式偏向于理論,相信看了本文上面的介紹大多都知道多多少少,第二種往往是很多Java程序員容易犯錯(cuò)的,接下來給出幾段代碼來講解。(環(huán)境是jdk1.8)
No.1
上面的代碼執(zhí)行完成之后除了123被輸出外,在此之前還輸出了“父類被初始化”,并沒有輸出子類被初始化。對于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化,這里通過子類來訪問父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類的初始化。這里可以開啟-XX:+TraceClassLoading虛擬機(jī)參數(shù)查看加載內(nèi)容。
No.2
上面的代碼執(zhí)行完之后并沒有任何輸出!也就是說此過程并沒有觸發(fā)jvm.FatherClass的初始化階段,但是實(shí)際上這個(gè)過程觸發(fā)了另一個(gè)名為[Lorg.FatherClass的類的初始化,它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于Object的子類,創(chuàng)建動(dòng)作由字節(jié)碼指令anewarray觸發(fā)。
[Lorg.FatherClass這個(gè)類代表了一個(gè)元素類型為jvm.FatherClass的一維數(shù)組,數(shù)組中應(yīng)有的屬性和方法(用戶可直接使用的只有被修飾為public的length屬性和clone方法)都實(shí)現(xiàn)在這個(gè)類里。
No.3
上面的代碼執(zhí)行結(jié)束之后控制臺只輸出了123,并沒有輸出“父類被初始化”,說明此時(shí)FatherClass并沒有被觸發(fā)初始化,這里和No.1里面value唯一不同就在于多加了一個(gè)final關(guān)鍵字。這是因?yàn)樵诰幾g階段以及做了常量傳播優(yōu)化,在編譯時(shí)就將常量value存儲(chǔ)到了Test類的常量池中,這里對value的引用其實(shí)就是對本類(Test)常量池的引用,所以這里無需初始化FatherClass類。也就是說,實(shí)際上Test的class文件之中并沒有FatherClass類的符號引用入口,這兩個(gè)類在編譯成class之后就不存在任何聯(lián)系了。
為了佐證該過程,我反編譯出No.1和No.3兩個(gè)測試代碼的Test字節(jié)碼文件
No.1情況下的Test.class
No.3情況下的Test.class
從字節(jié)碼中不難看出No.1情況獲取value值是通過getstatic #3獲取,其中#3是#3 = Fieldref #18.#19 // jvm/FatherClass.value:I,也就是說它仍然需要從FatherClass的引用獲取vlaue。而No.3情況獲取value值是bipush 123,這個(gè)123是直接從常量池中取的,無需從FatherClass類中獲取。
看完的朋友記得點(diǎn)贊噢!想學(xué)習(xí)更多的Java技術(shù)方面的知識的朋友們,可以進(jìn)我的Java高級架構(gòu)師交流群,里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識點(diǎn)的架構(gòu)資料,群號:680075317,也可以進(jìn)群一起交流,比如遇到技術(shù)瓶頸、面試不過的,大家一些交流學(xué)習(xí)!
參考文獻(xiàn):《深入理解Java虛擬機(jī)》