看完這篇后,別再說你不懂JVM類加載機制了~
JVM 通過雙親委派模型進行類的加載,即當某個類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
類加載器
- 啟動類加載器 (Bootstrap ClassLoader) :負責加載 JAVA_HOME\lib 目錄中的,或通過 - Xbootclasspath 參數指定路徑中的,且被虛擬機認可(按文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄也不會被加載)的類。啟動類加載器無法被 Java 程序直接引用;
- 擴展類加載器 (Extension ClassLoader) :負責加載 JAVA_HOME\jre\lib\ext 目錄中的,或通過 java.ext.dirs 系統變量指定路徑中的類庫;
- 應用程序類加載器 (Application ClassLoader) :負責加載用戶路徑(classpath)上的類庫。
- 通過繼承 java.lang.ClassLoader 類實現自定義類加載器(主要是重寫 findClass 方法)。
小結: 類加載器和字節碼是Java平臺無關性的基石,對于任意一個類,都需要由它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。
雙親委派模型的優點:
- 基礎類的統一加載問題(越基礎的類由越上層的加載器進行加載)。如類 java.lang.String,無論哪一個類加載器要加載這個類,最終都是委派給啟動類加載器進行加載,所以在程序的各種類加載器環境中都是同一個類。
- 提高 java 代碼的安全性。比如說用戶自定義了一個與系統庫里同名的 java.lang.String 類,那么這個類就不會被加載,因為最頂層的類加載器會首先加載系統的 java.lang.String 類,而不會加載自定義的 String 類,防止了惡意代碼的注入。
- 可以避免類的重復加載,另外也避免了 Java 的核心 API 被篡改。
類加載流程
類的生命周期會經歷以下 7 個階段:
加載階段
此階段用于查到相應的類(通過類名進行查找)并將此類的字節流轉換為方法區運行時的數據結構,然后再在內存中生成一個能代表此類的 java.lang.Class 對象,作為其他數據訪問的入口。
驗證階段
此步驟主要是為了驗證字節碼的安全性,如果不做安全校驗的話可能會載入非安全或有錯誤的字節碼,從而導致系統崩潰,它是 JVM 自我保護的一項重要舉措。
驗證的主要動作大概有以下幾個:
- 文件格式校驗包括常量池中的常量類型、Class 文件的各個部分是否被刪除或被追加了其他信息等;
- 元數據校驗包括父類正確性校驗(檢查父類是否有被 final 修飾)、抽象類校驗等;
- 字節碼校驗,此步驟最為關鍵和復雜,主要用于校驗程序中的語義是否合法且符合邏輯;
- 符號引用校驗,對類自身以外比如常量池中的各種符號引用的信息進行匹配性校驗。
準備階段
此階段是用來初始化并為類中定義的靜態變量分配內存的,這些靜態變量會被分配到方法區上。
HotSpot 虛擬機在 JDK 1.7 之前都在方法區,而 JDK 1.8 之后此變量會隨著類對象一起存放到 Java 堆中。
解析階段
此階段主要是用來解析類、接口、字段及方法的,解析時會把符號引用替換成直接引用。
所謂的符號引用是指以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可;而直接引用是可以直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。
符號引用和直接引用有一個重要的區別:使用符號引用時被引用的目標不一定已經加載到內存中;而使用直接引用時,引用的目標必定已經存在虛擬機的內存中了。
初始化
初始化階段 JVM 就正式開始執行類中編寫的 Java 業務代碼了。到這一步驟之后,類的加載過程就算正式完成了。
總結
如上圖所示,淺綠的兩個部分表示類的生命周期,就是從類的加載到類實例的創建與使用,再到類對象不再被使用時可以被 GC 卸載回收。
這里要注意一點,由 Java 虛擬機自帶的三種類加載器加載的類在虛擬機的整個生命周期中是不會被卸載的,只有用戶自定義的類加載器所加載的類才可以被卸載。