深入理解JVM虚拟机读书笔记——类的加载机制
注:本文參考自周志明老師的著作《深入理解Java虛擬機(jī)(第3版)》,相關(guān)電子書可以關(guān)注WX公眾號,回復(fù) 001 獲取。
Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這個過程被稱作虛擬機(jī)的類加載機(jī)制。
1. 類加載的時機(jī)
一個類型從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking)。這七個階段的發(fā)生順序如下圖(類的生命周期)所示:
2. 類加載的過程
接下來我們會詳細(xì)了解Java虛擬機(jī)中類加載的全過程,即加載、驗證、準(zhǔn)備、解析和初始化這五個階段所執(zhí)行的具體動作。
2.1 加載
“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段,在加載階段,Java虛擬機(jī)需要完成以下三件事情:
- 1)通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
- 2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)。
- 3)在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
2.2 驗證
驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會危害虛擬機(jī)自身的安全。
驗證階段可以分為如下幾個步驟:
1.文件格式驗證:
第一階段驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括下面這些驗證點:
- 是否以魔數(shù)0xCAFEBABE開頭。
- 主、次版本號是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)。
- 常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
- Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息。
- …
2.元數(shù)據(jù)驗證:
第二階段是對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合《Java語言規(guī)范》的要求,這個階段可能包括的驗證點如下:
- 這個類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)。
- 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
- 如果這個類不是抽象類,是否實現(xiàn)了其父類或接口之中要求實現(xiàn)的所有方法。
- 類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類型卻不同等)。
3.字節(jié)碼驗證:
第三階段是整個驗證過程中最復(fù)雜的一個階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語義是合法的、符合邏輯的。
在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗完畢以后,這階段就要對類的方法體(Class文件中的Code屬性)進(jìn)行校驗分析,保證被校驗類的方法在運(yùn)行時不會做出危害虛擬機(jī)安全的行為,例如:
- 保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會出現(xiàn)類似于“在操作棧放置了一個int類型的數(shù)據(jù),使用時卻按long類型來加載入本地變量表中”這樣的情況。
- 保證任何跳轉(zhuǎn)指令都不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。
- 保證方法體中的類型轉(zhuǎn)換總是有效的,例如可以把一個子類對象賦值給父類數(shù)據(jù)類型,這是安全的,但是把父類對象賦值給子類數(shù)據(jù)類型,甚至把對象賦值給與它毫無繼承關(guān)系、完全不相干的一個數(shù)據(jù)類型,則是危險和不合法的。
- …
4.符號引用驗證:
最后一個階段的校驗行為發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用[3]的時候,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類信息進(jìn)行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。本階段通常需要校驗下列內(nèi)容:
- 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
- 符號引用中的類、字段、方法的可訪問性(private、protected、public、< package>)是否可被當(dāng)前類訪問。
- …
2.3 準(zhǔn)備
準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段(是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值)。
假設(shè)一個類變量的定義為:
public static int value = 123;那變量value在準(zhǔn)備階段過后的初始值為0而不是123。
基本數(shù)據(jù)類型的零值表:
2.4 解析
解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
- 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。
- 直接引用(Direct References):直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。
解析的具體流程分為如下幾個階段:
- 1.類或接口的解析
- 2.字段解析
- 3.方法解析
- 4.接口方法解析
2.5 初始化
進(jìn)行準(zhǔn)備階段時,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會根據(jù)程序代碼去初始化類變量和其他資源(例如,靜態(tài)變量賦值動作和靜態(tài)語句塊(static{})中的語句)。
我們也可以從另外一種更直接的形式來表達(dá):初始化階段就是執(zhí)行類構(gòu)造器< clinit>()方法的過程。
3. 類加載器
類加載器雖然只用于實現(xiàn)類的加載動作,但它在Java程序中起到的作用卻遠(yuǎn)超類加載階段。對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。
這句話可以表達(dá)得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個Java虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
站在Java虛擬機(jī)的角度來看,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn)[1],是虛擬機(jī)自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java語言實現(xiàn),獨立存在于虛擬機(jī)外部,并且全都繼承自抽象類 java.lang.ClassLoader。
類加載器分類,以 JDK 8 為例:
| Bootstrap ClassLoader(啟動類加載器) | JAVA_HOME/jre/lib | 無法直接訪問 |
| Extension ClassLoader(擴(kuò)展類加載器) | JAVA_HOME/jre/lib/ext | 上級為 Bootstrap,顯示為 null |
| Application ClassLoader(應(yīng)用程序類加載器) | classpath | 上級為 Extension |
| 自定義類加載器 | 自定義 | 上級為 Application |
類加載器的優(yōu)先級(由高到低):啟動類加載器 -> 擴(kuò)展類加載器 -> 應(yīng)用程序類加載器 -> 自定義類加載器
3.1 啟動類加載器
用 Bootstrap 類加載器加載類:
package cn.itcast.jvm.t3.load;public class F {static {System.out.println("bootstrap F init");} }執(zhí)行:
package cn.itcast.jvm.t3.load;public class Load5_1 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");// aClass.getClassLoader():獲得aClass對應(yīng)的類加載器System.out.println(aClass.getClassLoader());} }輸出:
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init null- -Xbootclasspath 表示設(shè)置 bootclasspath
- 其中 /a:. 表示將當(dāng)前目錄追加至 bootclasspath 之后
- 可以有以下幾個方式替換啟動類路徑下的核心類:
- java -Xbootclasspath: < new bootclasspath>
- 前追加:java -Xbootclasspath/a:<追加路徑>
- 后追加:java -Xbootclasspath/p:<追加路徑>
3.2 擴(kuò)展類加載器
package cn.itcast.jvm.t3.load;public class G {static {System.out.println("classpath G init");} }程序執(zhí)行:
public class Load5_2 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");System.out.println(aClass.getClassLoader());} }輸出結(jié)果:
classpath G init sun.misc.Launcher$AppClassLoader@18b4aac2 // 這個類是由應(yīng)用程序加載器加載寫一個同名的類:
package cn.itcast.jvm.t3.load;public class G {static {System.out.println("ext G init");} }打個 jar 包:
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class // 將G.class打jar包 已添加清單 正在添加: cn/itcast/jvm/t3/load/G.class(輸入 = 481) (輸出 = 322)(壓縮了 33%)將 jar 包拷貝到JAVA_HOME/jre/lib/ext(擴(kuò)展類加載器加載的類必須是以jar包方式存在),重新執(zhí)行 Load5_2
輸出:
ext G init sun.misc.Launcher$ExtClassLoader@29453f44 // 這個類是由擴(kuò)展類加載器加載3.3 應(yīng)用程序加載器
應(yīng)用程序類加載器(Application Class Loader):這個類加載器由 sun.misc.Launcher$AppClassLoader 來實現(xiàn)。由于應(yīng)用程序類加載器是ClassLoader類中的getSystem-ClassLoader() 方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
3.4 自定義類加載器
什么時候需要自定義類加載器:
- 1)想加載非 classpath 隨意路徑中的類文件
- 2)都是通過接口來使用實現(xiàn),希望解耦時,常用在框架設(shè)計
- 3)這些類希望予以隔離,不同應(yīng)用的同名類都可以加載,不沖突,常見于 tomcat 容器
步驟:
- 繼承 ClassLoader 父類。
- 要遵從雙親委派機(jī)制,重寫 findClass 方法 注意不是重寫 loadClass 方法,否則不會走雙親委派機(jī)制。
- 讀取類文件的字節(jié)碼。
- 調(diào)用父類的 defineClass 方法來加載類。
- 使用者調(diào)用該類加載器的 loadClass 方法。
4. 雙親委派模型
什么是雙親委派模型?
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
如下圖所示:
為什么要使用雙親委派模型呢?(好處)
避免重復(fù)加載 + 避免核心類篡改:
- 采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載,當(dāng)父加載器已經(jīng)加載了該類時,就沒有必要子加載器再加載一次。
- 其次是考慮到安全因素,java 核心 api 中定義類型不會被隨意替換,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為 java.lang.Integer 的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類,發(fā)現(xiàn)該類已被加載,并不會重新加載網(wǎng)絡(luò)傳遞的過來的 java.lang.Integer,而直接返回已加載過的 Integer.class,這樣便可以防止核心API庫被隨意篡改。
結(jié)語:
非常建議學(xué)習(xí)Java的小伙伴,買一本周志明老師的《深入理解Java虛擬機(jī)(第3版)》去讀一讀,博客和視頻教程,始終不如看書來得實在呀!
后續(xù)會陸續(xù)更新,這本書的筆記記的差不多了,排版和格式需要花時間整理,文章都會同步到公眾號上,也歡迎大家通過公眾號加入我的交流qun互相討論jvm這塊的知識內(nèi)容!
總結(jié)
以上是生活随笔為你收集整理的深入理解JVM虚拟机读书笔记——类的加载机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php中二进制函数,PHP-----函数
- 下一篇: 启动vpn报网络扩展错误(问题篇)