在介紹完class文件格式后瓤漏,我們來看下虛擬機(jī)是如何把一個(gè)由class文件描述的類加載到內(nèi)存中的。具體來說java中類的加載涉及7個(gè)階段:加載蝶俱、校驗(yàn)、準(zhǔn)備饥漫、解析榨呆、初始化、使用积蜻、卸載。
1.加載時(shí)機(jī)
并不是所有的類在程序啟動(dòng)時(shí)即被加載竿拆,為提升效率,虛擬機(jī)通常秉承的是按需加載的原則宾尚,即需要使用到相應(yīng)的類時(shí)才加載對(duì)應(yīng)的類丙笋。具體包括如下幾個(gè)加載時(shí)機(jī):
- 遇到new、getstatic央勒、putstatic、invokestatic這4條指令時(shí)稳吮,如果對(duì)應(yīng)的類沒有被加載,虛擬機(jī)會(huì)首先加載對(duì)應(yīng)的類灶似。這4條指令對(duì)應(yīng)的場(chǎng)景是:
- 創(chuàng)建一個(gè)實(shí)例對(duì)象
- 訪問一個(gè)類的靜態(tài)變量(注意:不包括被final修飾,在編譯時(shí)已被放入常量池的變量)
- 執(zhí)行一個(gè)類的靜態(tài)方法
- 使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用時(shí)酪惭,如果相應(yīng)類未被加載,則虛擬機(jī)會(huì)加載該類
- 初始化子類時(shí)如果其父類尚未被加載春感,虛擬機(jī)會(huì)先加載其父類
- 虛擬機(jī)啟動(dòng)時(shí),包含main方法的類會(huì)被加載
- 使用JDK 1.7動(dòng)態(tài)語(yǔ)言支持時(shí)嫩实,某些場(chǎng)景會(huì)觸發(fā)類加載
- 加載
加載是整個(gè)類加載的一個(gè)過程窥岩,具體來說加載階段一共做了三項(xiàng)工作:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將字節(jié)流中的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為運(yùn)行中的實(shí)際數(shù)據(jù)結(jié)構(gòu)并存儲(chǔ)在方法區(qū)中
- 為該類生成一個(gè)java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
3.驗(yàn)證
驗(yàn)證階段的目的就是保證Class文件的字節(jié)流中包含的信息都符合當(dāng)前虛擬機(jī)的要求颂翼,不會(huì)危害虛擬機(jī)本身的安全。具體來說驗(yàn)證階段的工作主要分為以下幾部分:
3.1 文件格式驗(yàn)證
- 是否已0xCAFEBABE開頭
- 主次版本是否在當(dāng)前虛擬機(jī)可以處理的范圍內(nèi)
- 常量池中的數(shù)據(jù)是否有不被支持的類型
- 指向常量的各種索引值是否有指向不存在常量或不符要求的常量
- …...
3.2 元數(shù)據(jù)驗(yàn)證
- 當(dāng)前加載類是否有父類
- 是否繼承了不被允許繼承的類(final類)
- 如果不是抽象類球及,是否實(shí)現(xiàn)了父類中所有要求實(shí)現(xiàn)的方法
- …...
3.3 字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證是整個(gè)驗(yàn)證過程中最為復(fù)雜的一步集歇,主要的目的是通過分析數(shù)據(jù)流和控制流,確定語(yǔ)義是合法的诲宇、符合邏輯的,例如:
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 保證方法體內(nèi)的類型轉(zhuǎn)換是有效的
- …...
3.4 符號(hào)引用驗(yàn)證
- 符號(hào)引用中通過字符串描述的全限定名是否能夠找到對(duì)應(yīng)的類
- 符號(hào)引用中的類姑蓝、字段、方法的訪問性(private纺荧、protected旭愧、public宙暇、default)是否可被當(dāng)前的類訪問
4 準(zhǔn)備
正式為類變量分配內(nèi)存并設(shè)置其初始值,這些變量所使用的內(nèi)存都將在方法區(qū)進(jìn)行分配占贫。
5 解析
解析是虛擬機(jī)將class文件中常量池中的符號(hào)引用解析為直接應(yīng)用的過程。
- 符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo)瞳收,符號(hào)可以是任意形式的字面量,只要使用時(shí)能無歧義的定位到目標(biāo)即可螟深,與虛擬機(jī)的內(nèi)存布局無關(guān)。
- 直接引用:直接引用可以直接訪問到存在于內(nèi)存中的目標(biāo)凡蜻,可以是一個(gè)直接指針也可以是一個(gè)句柄。
解析過程主要涉及以下幾個(gè)步驟:
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
- 方法類型解析
- 方法句柄解析
- 調(diào)用點(diǎn)限定符解析
6 初始化
初始化就是執(zhí)行類構(gòu)造器方法<cinit>()的過程咽瓷,<cinit>()方法是由編譯器自動(dòng)收集的所有類變量的賦值動(dòng)作以及靜態(tài)語(yǔ)塊合并生成的舰讹。
7 類加載器
上述的類加載過程都是由java虛擬機(jī)的類加載器完成的闪朱。對(duì)于任意一個(gè)類,都需要有加載它的類加載器和這個(gè)類本身一同確立其在java虛擬機(jī)中的唯一性奋姿,每一個(gè)類加載器都擁有一個(gè)獨(dú)立的類命名空間。事實(shí)上Java程序在運(yùn)行時(shí)存在不止一種類加載器萍悴,絕大部分Java程序都會(huì)使用到以下三種類加載器:
- 啟動(dòng)類加載器:用于加載<JAVA_HOME>/lib路徑下的類
- 擴(kuò)展加載器:用于加載<JAVA_HOME>/lib/ext路徑下的類
- 應(yīng)用程序類加載器:復(fù)雜加載用戶應(yīng)用程序路徑上的類
如果有需要寓免,開發(fā)人員還可以加入自定義的類加載器。既然存在如此多的類加載器袜香,那么當(dāng)一個(gè)類需要加載時(shí),具體是由那個(gè)類進(jìn)行加載呢蜈首?由于所有的類加載器都遵守“雙親委派模型”,所以虛擬機(jī)在運(yùn)行期間可以保證一個(gè)類只會(huì)被加載一次吆寨。
雙親委派模型的工作過程:如果一個(gè)類加載器收到了類加載的請(qǐng)求踩寇,它會(huì)把這個(gè)請(qǐng)求交給自己的父類加載器去完成,父類加載器也會(huì)繼續(xù)上自己的父類加載器發(fā)送請(qǐng)求盒延,依次類推缩擂。如果父類已經(jīng)加載過該類添寺,則當(dāng)前加載器會(huì)直接返回已加載的類,只有當(dāng)父類沒有加載過該類時(shí)博脑,當(dāng)前類加載器才會(huì)真正去加載該類票罐。