JVM類加載機制
@(Java)[JVM|類文件結(jié)構(gòu)]
[TOC]
基本介紹
- JVM的類加載機制:JVM把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被JVM直接使用的Java類型。
Java中,類型的加載和連接過程都是在程序運行期間完成的。這樣的特性體現(xiàn)在Java的動態(tài)擴展特性。(例如編寫一個使用接口的應(yīng)用程序,可以等到運行時再指定實際的實現(xiàn))
類加載的時機
- 類從被加載到JVM,到卸載出內(nèi)存,整個生命周期包括;加載、驗證、準備、解析、初始化、使用和卸載七個階段。其中驗證、準備和解析三個部分統(tǒng)稱為連接。

類加載順序
- 其中,加載、驗證、準備、初始化和卸載的順序是確定的。但是解析階段不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java的運行時綁定(動態(tài)綁定)。
- 什么時候開始類加載過程的第一階段:加載?
JVM并沒有強制規(guī)定,可以交給JVM的具體實現(xiàn)自由把握。 - 什么時候開始類加載過程的初始化階段?
JVM規(guī)定有且只有四種情況必須立即對類進行初始化。
1. 遇到new、getstatic、putstatic或invokestatic命令時,如果類沒有進行過初始化,則需要先觸發(fā)初始化。常見的場景是:使用new實例化對象、讀取或設(shè)置一個類的靜態(tài)字段,調(diào)用一個類的金泰方法。
2. 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候。
3. 當初始化一個類的時候,如果其父類還沒有被初始化,則需要觸發(fā)父類的初始化。
4. 當JVM啟動,需要執(zhí)行main方法的主類,需要被觸發(fā)初始化。
- 對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此,通過子類來引用父類中的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。
- 通過數(shù)組定義來引用類,并不會觸發(fā)此類的初始化。
- 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化。
類加載的過程
加載
在加載階段,JVM需要完成的工作:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
“通過一個類的全限定名來獲取定義此類的二進制字節(jié)流”,并沒有指明二進制字節(jié)流是否要從Class文件獲取。因此在加載階段,可以使用系統(tǒng)提供的類加載器完成,也可以由用戶自定義的類加載器完成,控制獲取字節(jié)流的方式。
加載階段和連接階段交叉進行。
驗證
驗證是連接階段的第一步,目的是為了確保Class文件的字節(jié)流包含的信息是否符合JVM要求。
大致包含四個驗證過程:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證。
- 文件格式驗證
驗證字節(jié)流是否符合Class文件格式規(guī)范,并且能被當前版本的JVM處理。 - 元數(shù)據(jù)驗證
對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求,主要驗證點有:- 這個類是否有父類。
- 這個類的父類是否繼承了不可被繼承的類。
- 如果這個類不是抽象類,是否實現(xiàn)了其父類或者接口要求實現(xiàn)的所有方法。
- 類中的字段、方法是否與父類產(chǎn)生了矛盾。
- 字節(jié)碼驗證
進行數(shù)據(jù)流和控制流分析,保證被校驗類的方法在運行時不會有危害JVM安全的行為。 - 符號引用驗證
發(fā)生在JVM將符號引用轉(zhuǎn)化為直接引用的時候。主要的驗證點如下:- 符號引用中通過字符串描述的全限定名能否找到對應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
- 符號引用中的類、字段和方法的訪問性是否可被當前類訪問。
準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段。這些內(nèi)存都將在方法區(qū)中進行分配。
- 這個時候進行內(nèi)存分配的僅包括類變量,而不包括實例變量。實例變量在對象實例化的時候隨著對象一起分配在Java堆中。
- 這里說的初始值通常情況下是數(shù)據(jù)類型的零值。
解析
解析階段是JVM將常量池內(nèi)的符號引用替換為直接引用的過程。
- 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,其引用的目標并不一定已經(jīng)加載在內(nèi)存中。
- 直接引用:直接引用直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。如果有了直接引用,那么引用的目標必定已經(jīng)在內(nèi)存中。
JVM可能會對第一次解析的結(jié)果進行緩存,從而避免解析動作重復進行。
- 解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用進行。
初始化
在準備階段,變量已經(jīng)賦過一次值,而在初始化階段,則根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源。換句話說,初始化階段是執(zhí)行clinit方法的過程。
- clinit方法是由編譯器自動收集類中所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的。編譯器收集的順序由語句在源文件中出現(xiàn)的順序決定。靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊中可以賦值,但不能訪問。
- clinit方法與類的構(gòu)造方法init不同,它不需要顯示的調(diào)用父類構(gòu)造方法,JVM會保證子類的clinit方法執(zhí)行之前,父類的clinit方法已經(jīng)執(zhí)行完畢。因此在JVM中第一個被執(zhí)行的clinit方法的類一定是java.lang.Object.
- 由于父類的clinit方法先執(zhí)行,意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類變量的賦值操作。
- clinit方法對于類或者接口來說不是必須的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為該類生成clinit方法。
- 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成clinit方法。但不同的是,執(zhí)行接口的clinit方法時不需要執(zhí)行父接口的clinit方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現(xiàn)累在初始化時也一樣不會執(zhí)行接口的clinit方法。
- JVM保證一個類的clinit方法在多線程環(huán)境中被正確的加鎖和同步。如果多個線程同時初始化一個類,那么只會有一個線程執(zhí)行這個類的clinit方法,其他線程都需要阻塞等待。