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