1.類(lèi)加載的目的:
一份被javac編譯過(guò)的class文本文件通過(guò)加載,生成某種形式的Class數(shù)據(jù)結(jié)構(gòu)進(jìn)入內(nèi)存, 程序可以調(diào)用這個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)構(gòu)造出object,這個(gè)過(guò)程是在運(yùn)行時(shí)進(jìn)行的,也是java動(dòng)態(tài)拓展性的根基。
一個(gè)類(lèi)的生命周期:
- javac 編譯
- 加載
- 鏈接
- 初始化
- 使用
- 卸載
類(lèi)加載包含了三個(gè)階段
- 加載
- 鏈接
- 初始化
鏈接包含三個(gè)步驟:
- 驗(yàn)證
- 準(zhǔn)備
- 解析
其中,解析步驟是靈活的,他可以在初始化之前或者之后再進(jìn)行,實(shí)現(xiàn)所謂的‘后期綁定’,其他環(huán)節(jié)則是不可改變的。
2.類(lèi)加載的階段
加載
加載是一個(gè)讀取Class文件,將其轉(zhuǎn)化為某種靜態(tài)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)在方法區(qū)內(nèi),并在堆中生成一個(gè)便于用戶調(diào)用的java.lang.Class類(lèi)型的對(duì)象的過(guò)程。
注意:
這里的Class文件不僅僅是指的本地文件,泛指各種來(lái)源的二進(jìn)制流,比如從網(wǎng)絡(luò)、數(shù)據(jù)庫(kù)、即時(shí)計(jì)算出來(lái)的class文件,比如著名的就是 動(dòng)態(tài)代理技術(shù),就是使用到了即時(shí)計(jì)算出來(lái)的class,然后實(shí)例化代理對(duì)象,可以自由選擇二進(jìn)制流。
鏈接
- 驗(yàn)證
驗(yàn)證動(dòng)作是有很多個(gè)步驟的,包括:文件格式驗(yàn)證、元數(shù)據(jù)字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。
文件格式驗(yàn)證:其實(shí)是發(fā)生在加載階段的,如果通過(guò),那么才能順利加載,順利加載后此時(shí)方法區(qū)內(nèi)雖然已經(jīng)存在了該class的靜態(tài)結(jié)構(gòu),堆中也存在了該class類(lèi)型的對(duì)象,但是這并不代表著JVM已經(jīng)完全認(rèn)可了這個(gè)類(lèi)。如果程序想要使用這個(gè)類(lèi)那么就必須進(jìn)行鏈接,而鏈接的第一步就是進(jìn)一步對(duì)這個(gè)類(lèi)進(jìn)行驗(yàn)證,到底對(duì)方法區(qū)內(nèi)的class靜態(tài)結(jié)構(gòu)進(jìn)行了哪些方面的驗(yàn)證。
元數(shù)據(jù)的驗(yàn)證、字節(jié)碼的驗(yàn)證:就是對(duì)class的靜態(tài)結(jié)構(gòu)進(jìn)行語(yǔ)法和語(yǔ)義上的分析,保證其不會(huì)產(chǎn)生危害虛擬機(jī)的行為。這兩個(gè)步驟驗(yàn)證通過(guò),那么虛擬機(jī)會(huì)姑且認(rèn)為該class是安全的,但是這并不意味著驗(yàn)證已經(jīng)完全結(jié)束了,還有一道對(duì)符號(hào)引用進(jìn)行驗(yàn)證的步驟。
符號(hào)引用驗(yàn)證:它是在解析階段內(nèi)發(fā)生的,而解析階段我們之前也提到過(guò),它可以在初始化階段之前或者之后進(jìn)行。
驗(yàn)證其實(shí)包含了很多的步驟,分散在各個(gè)不同的階段內(nèi),驗(yàn)證的內(nèi)容是會(huì)不斷發(fā)展的,除了這里提到的文本格式驗(yàn)證、元數(shù)據(jù)字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證 四個(gè)環(huán)節(jié) 從低版本的虛擬機(jī)到現(xiàn)在 驗(yàn)證步驟其實(shí)已經(jīng)不斷的加入了各種機(jī)制。在未來(lái),虛擬機(jī)的開(kāi)發(fā)人員可能會(huì)引入更多更完善更完美的策略。
- 準(zhǔn)備
在元數(shù)據(jù)、字節(jié)碼驗(yàn)證通過(guò)之后,虛擬機(jī)會(huì)姑且認(rèn)為該class是安全的,這時(shí)候會(huì)進(jìn)入準(zhǔn)備階段,準(zhǔn)備階段做的處理,并不復(fù)雜,就是為該類(lèi)型中定義的靜態(tài)變量賦0值,注意,這里僅僅是靜態(tài)變量而不是成員變量,此時(shí)將會(huì)出現(xiàn)一個(gè)被太多人混淆和誤解的概念,我們這里就好好捋一捋。
虛擬機(jī)內(nèi)存規(guī)范中的定義了方法區(qū)這種抽象概念,HotSpot這種主流虛擬機(jī)在JDK8之前使用了永久代這種具體的實(shí)現(xiàn)方式來(lái)實(shí)現(xiàn)方法區(qū)。在JDK8之后,棄用‘永久代’這種實(shí)現(xiàn)方式,采用元空間這種直接內(nèi)存取代。
在JDK8之前,類(lèi)的元信息、常量池、靜態(tài)變量等都存儲(chǔ)在 永久代這種具體實(shí)現(xiàn)中,而在JDK8及之后,常量池、靜態(tài)變量被移除‘方法區(qū)’,轉(zhuǎn)移到了堆中,元信息這些呢,依然保留在方法區(qū)內(nèi),但是具體的存儲(chǔ)方式改成了元空間。
- 解析
該階段主要做的事情是將 符號(hào)引用 替換為 直接引用。什么是符號(hào)引用,什么是直接引用?
當(dāng)一個(gè)Java類(lèi)被編譯成Class之后,假如這個(gè)類(lèi)稱為A,并且A中 引用了B,那么在編譯階段,A是不知道B有沒(méi)有被編譯的,而且此時(shí)B也一定沒(méi)有被加載,所以A肯定不知道B的實(shí)際地址,那么A怎么才能找到B呢?,此時(shí)在A的class文件中,將使用一個(gè)字符串S來(lái)代表B的地址,S就被稱為符號(hào)引用,在運(yùn)行時(shí)呢,如果A發(fā)生了類(lèi)加載,到了解析階段會(huì)發(fā)現(xiàn)B還未被加載,那么將會(huì)觸發(fā)B的類(lèi)加載,將B加載到虛擬機(jī)中,此時(shí)A中的B的符號(hào)引用將會(huì)被替換成B的實(shí)際地址,這被稱為直接引用,這樣也就能夠真正的調(diào)用B了。
Java通過(guò)后期綁定的方式來(lái)實(shí)現(xiàn)多態(tài),那么后期綁定這個(gè)概念,又是如何實(shí)現(xiàn)的呢?
其實(shí)就是這里的動(dòng)態(tài)解析,如果A調(diào)用的B是一個(gè)具體的實(shí)現(xiàn)類(lèi),那么就稱為靜態(tài)解析,因?yàn)榻忉尩哪繕?biāo)類(lèi)型很明確,而假如上層Java代碼使用了多態(tài),這里的B是一個(gè)抽象類(lèi)或者是接口,那么 B可能有兩個(gè)具體的實(shí)現(xiàn)類(lèi)C和D 此時(shí),B的具體實(shí)現(xiàn)并不明確,當(dāng)然也就不知道使用哪個(gè)具體類(lèi)的直接引用來(lái)進(jìn)行替換,既然不知道,那么就等一等吧,直到運(yùn)行過(guò)程中發(fā)生了調(diào)用,此時(shí)虛擬機(jī)調(diào)用棧中將會(huì)得到具體的類(lèi)型信息,這時(shí)候再進(jìn)行解析,就能用明確的直接引用來(lái)替換符號(hào)引用,這就解釋了為什么解析階段有時(shí)候會(huì)發(fā)生在初始化階段之后,這就是動(dòng)態(tài)解析,用它來(lái)實(shí)現(xiàn)了后期綁定。底層對(duì)應(yīng)了invokedynamic這條字節(jié)碼指令。
擴(kuò)展:多態(tài)
擴(kuò)展:動(dòng)態(tài)綁定
初始化
初始化階段簡(jiǎn)單概括:此時(shí)會(huì)判斷代碼中是否存在主動(dòng)的資源初始化操作,如果有的話,那么執(zhí)行。這里所說(shuō)的主動(dòng)的資源初始化動(dòng)作,不是指的構(gòu)造函數(shù),而是class層面的,比如說(shuō)成員變量的賦值動(dòng)作,靜態(tài)變量的賦值動(dòng)作,以及靜態(tài)代碼塊的邏輯。而只有顯式的調(diào)用new指令 才會(huì)調(diào)用構(gòu)造函數(shù)進(jìn)行對(duì)象的實(shí)例化,這是對(duì)象層面的,二者不能進(jìn)行混淆。