JVM 中類(lèi)加載的鏈接與初始化
1. 前言
對(duì)于類(lèi)加載子系統(tǒng),前邊的課程已經(jīng)對(duì)加載(Loading)這一步做了詳細(xì)的講解,本節(jié)主要對(duì)類(lèi)加載子系統(tǒng)加載步驟中的鏈接與初始化進(jìn)行講解。本節(jié)主要知識(shí)點(diǎn)如下:
- 鏈接(Linking)步驟更加詳細(xì)的模塊劃分:驗(yàn)證,準(zhǔn)備和解析,為本節(jié)基礎(chǔ)知識(shí)點(diǎn);
- 掌握在鏈接(Linking)步驟中的第一步驗(yàn)證的詳細(xì)驗(yàn)證內(nèi)容,為本節(jié)重點(diǎn)內(nèi)容之一;
- 掌握在鏈接(Linking)步驟中的第二步準(zhǔn)備的準(zhǔn)備內(nèi)容,為本節(jié)重點(diǎn)內(nèi)容之一;
- 掌握在鏈接(Linking)步驟中的第三步解析的具體解析內(nèi)容,為本節(jié)重點(diǎn)內(nèi)容之一;
- 掌握初始化(Init)步驟中的規(guī)則以及實(shí)例初始化順序,為本節(jié)重點(diǎn)內(nèi)容之一。
通篇皆為重點(diǎn)內(nèi)容,本節(jié)知識(shí)也會(huì)為類(lèi)加載子系統(tǒng)部分畫(huà)上一個(gè)完美的句號(hào),一定要認(rèn)真對(duì)待。
2. 類(lèi)加載子系統(tǒng)知識(shí)回顧
我們?cè)贘VM 總體架構(gòu)的講解過(guò)程中,提到過(guò)類(lèi)加載子系統(tǒng)的工作流程分為三步:加載->鏈接->初始化。如下圖所示:
本節(jié)我們所討論的內(nèi)容都是圍繞第二步“鏈接(Linking)” 和第三步“初始化(Init)”進(jìn)行的。
我們將鏈接(Linking)這一步,再進(jìn)行下細(xì)致的模塊劃分,如下圖所示:
從上圖中我們可看到,鏈接(Linking)這一步,里邊包含了三個(gè)更加細(xì)致的步驟,分別為驗(yàn)證(verify),準(zhǔn)備(prepare)和解析(resolve)。后文我們會(huì)對(duì)這三個(gè)步驟進(jìn)行講解。
3. 鏈接-驗(yàn)證(verify)
定義:驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并不會(huì)危害虛擬機(jī)的自身安全。
驗(yàn)證過(guò)程的主要驗(yàn)證信息:驗(yàn)證過(guò)程中,主要對(duì)三種類(lèi)型的數(shù)據(jù)進(jìn)行驗(yàn)證,分別是“元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證”。具體內(nèi)容請(qǐng)看下邊的講解。
元數(shù)據(jù)驗(yàn)證:
- 驗(yàn)證這個(gè)類(lèi)是否有父類(lèi)(除了 java.lang.Object 之外,所有類(lèi)都應(yīng)當(dāng)有父類(lèi));
- 驗(yàn)證這個(gè)類(lèi)是否繼承了不允許被繼承的類(lèi)(被 final 修飾的類(lèi));
- 如果這個(gè)類(lèi)不是抽象類(lèi),驗(yàn)證該類(lèi)是否實(shí)現(xiàn)了其父類(lèi)或接口之中所要求實(shí)現(xiàn)的所有方法;
- 驗(yàn)證類(lèi)中的字段、方法是否與父類(lèi)產(chǎn)生矛盾(例如覆蓋了父類(lèi)的 final 字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類(lèi)型卻不同等等)。
字節(jié)碼驗(yàn)證:字節(jié)碼驗(yàn)證主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。這個(gè)階段將對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)產(chǎn)生危害虛擬機(jī)安全的事件,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列都能配合工作。例如不會(huì)出現(xiàn)類(lèi)似這樣的情況:在操作數(shù)棧放置了一個(gè)int類(lèi)型的數(shù)據(jù),使用時(shí)卻按long類(lèi)型來(lái)加載入本地變量表中;
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上;
- 保證方法體中的類(lèi)型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類(lèi)對(duì)象賦值給父類(lèi)數(shù)據(jù)類(lèi)型,但是把父類(lèi)對(duì)象賦值給子類(lèi)數(shù)據(jù)類(lèi)型,甚至把對(duì)象賦值給與它毫無(wú)繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類(lèi)型,則是危險(xiǎn)不合法的。
符號(hào)引用驗(yàn)證:符號(hào)引用驗(yàn)證可以看作是類(lèi)對(duì)自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),通常需要校驗(yàn)以下內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能夠找到對(duì)應(yīng)的類(lèi);
- 在指定類(lèi)中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱(chēng)所描述的方法和字段;
- 符號(hào)引用中的類(lèi)、字段、方法的訪問(wèn)性(private、default、protected、public)是否可被當(dāng)前類(lèi)訪問(wèn)。
4. 鏈接-準(zhǔn)備(prepare)
定義:準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量默認(rèn)值(通常情況下是數(shù)據(jù)類(lèi)型的零值)的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化的時(shí)候隨著對(duì)象一起分配在Java堆中。
Tips:準(zhǔn)備階段是設(shè)置類(lèi)變量的默認(rèn)值,不同類(lèi)型的類(lèi)變量的默認(rèn)值是不同的。變量默認(rèn)值的對(duì)照表請(qǐng)參看下表:
變量類(lèi)型
默認(rèn)值
int
0
long
0L
short
0
char
‘\u0000’
byte
0
boolean
false
folat
0.0f
double
0.0d
reference
null
5. 鏈接-解析(resolve)
定義:解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。
Tips:定義中又引出了兩個(gè)新的概念:符號(hào)引用和直接引用。想要理解解析,必須要先搞明白什么是符號(hào)引用和直接引用。
符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。
直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。如果有了直接引用,那么引用的目標(biāo)一定是已經(jīng)存在于內(nèi)存中。
解析過(guò)程具體的解析內(nèi)容:解析過(guò)程中,主要對(duì)如下4種類(lèi)型的數(shù)據(jù)進(jìn)行驗(yàn)證:
- 類(lèi)或接口的解析;
- 字段解析;
- 類(lèi)方法解析;
- 接口方法解析。
6. 初始化
定義:進(jìn)行準(zhǔn)備階段時(shí),變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始零值,而在初始化階段,則會(huì)根據(jù)程序員通過(guò)程序編碼制定的主觀計(jì)劃去初始化類(lèi)變量和其他資源。
類(lèi)的初始化階段是類(lèi)加載過(guò)程的最后一個(gè)步驟,之前介紹的幾個(gè)類(lèi)加載的動(dòng)作里,除了在加載階段用戶(hù)應(yīng)用程序可以通過(guò)自定義類(lèi)加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)來(lái)主導(dǎo)控制。直到初始化階段,Java虛擬機(jī)才真正開(kāi)始執(zhí)行類(lèi)中編寫(xiě)的 Java 程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
實(shí)例的初始化順序:在進(jìn)行初始化時(shí),實(shí)例變量的初始化順序如下圖所示:
實(shí)例的初始化順序是非常重要的知識(shí)點(diǎn),在面試過(guò)程中也經(jīng)常涉及到這個(gè)知識(shí)點(diǎn),上圖的加載順序需要重點(diǎn)掌握。
7. 小結(jié)
到目前為止,類(lèi)加載器子系統(tǒng)就全部講解完成了。我們學(xué)習(xí)了類(lèi)的加載,三種類(lèi)加載器,雙親委派模型以及本節(jié)所講述的鏈接與初始化,其中對(duì)鏈接有細(xì)分了三個(gè)步驟進(jìn)行了講解。
類(lèi)加載器子系統(tǒng)是非常重要的 JVM 模塊,需要用心學(xué)習(xí),對(duì)于一些概念性知識(shí)要增強(qiáng)理解,原理性知識(shí)要深入思索。后續(xù)我們會(huì)繼續(xù)講解 JVM 的其他重要模塊。