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