JVM 系列 - 類加載器機制#
內容整理自:
一、簡述#
虛擬機把描述類的數據從 class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制
二、類的加載過程以及生命周期#
類加載的過程分為三個步驟 (五個階段) :加載
-> 連接(驗證、準備、解析)
-> 初始化
加載#
加載的過程描述:
- 通過類的全限定名定位.class 文件,並獲取其二進制字節流。
- 把字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構。
- 在 Java 堆中生成一個此類的 java.lang.Class 對象,作為方法區中這些數據的訪問入口
連接#
連接:包括驗證、準備、解析三步
驗證#
驗證是連接階段的第一步,用於確保 Class 字節流中的信息是否符合虛擬機的要求
具體驗證形式
文件格式驗證
:驗證字節流是否符合 Class 文件格式的規範;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。元數據驗證
:對字節碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object 之外。字節碼驗證
:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。符號引用驗證
:確保解析動作能正確執行
準備#
為類的靜態變量分配內存
,並將其初始化為默認值。準備過程通常分配一個結構用來存儲類信息,這個結構中包含了類中定義的成員變量,方法和接口信息等
具體行為:
- 這時候進行內存分配的
僅包括類變量(static)
,而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。 - 這裡所設置的初始值通常情況下
是數據類型默認的零值
(如 0、0L、null、false 等),而不是在 Java 代碼中被顯式賦值的 (被顯式賦值的常量
例外)
解析#
解析:把類中對常量池內的符號引用
轉換為直接引用
符號引用 (Symbolic References): 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要可以唯一定位到目標即可。符號引用於內存佈局無關,所以所引用的對象不一定需要已經加載到內存中。各種虛擬機實現的內存佈局可以不同,但是接受的符號引用必須是一致的,因為符號引用的字面量形式已經明確定義在 Class 文件格式中
直接引用 (Direct References): 直接引用時直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用和虛擬機實現的內存佈局相關,同一個符號引用在不同虛擬機上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼它一定已經存在於內存中了
常量池中常量類型:
- 常量池中常量數量不固定,因此常量池開頭放置一個 u2 類型的無符號數,用來存儲當前常量池的容量。
- 常量池的每一項常量都是一個表,表開始的第一位是一個 u1 類型的標誌位(tag),代表當前這個常量屬於哪種常量類型
類型 | tag | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8 編碼的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 標識方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
解析動作主要針對類或接口
、字段
、類方法
、接口方法
、方法類型
、方法句柄
和調用點限定符
等 7 類符號引用進行
初始化#
初始化:對類靜態變量賦予正確的初始值
初始化的目標#
- 實現對聲明類靜態變量時指定的初始值的初始化;
- 實現對使用靜態代碼塊設置的初始值的初始化。
初始化的步驟#
- 如果此類沒被加載、連接,則先加載、連接此類;
- 如果此類的直接父類還未被初始化,則先初始化其直接父類;
- 如果類中有初始化語句,則按照順序依次執行初始化語句。
初始化的時機#
其中情況 1 中的 4 條字節碼指令在 Java 裡最常見的場景是:
- new 一個對象時
- set 或者 get 一個類的靜態字段(除去那種被 final 修飾放入常量池的靜態字段)
- 調用一個類的靜態方法
Java 中父類和子類初始化順序#
- 父類中靜態成員變量和靜態代碼塊
- 子類中靜態成員變量和靜態代碼塊
- 父類中普通成員變量和代碼塊,父類的構造函數
- 子類中普通成員變量和代碼塊,子類的構造函數
類的主動引用與被動引用#
在 java 虛擬機規範中,嚴格規定了,只有對類進行主動引用,才會觸發其初始化方法。而除此之外的引用方式稱之為被動引用,不會觸發類的初始化方法
主動引用
主動引用:在類加載階段,只執行加載、連接操作,不執行初始化操作
被動引用
主動引用之外的引用情況都稱之為被動引用,這些引用不會進行初始化
被動引用的幾種形式:
- 通過子類引用父類的靜態字段,不會導致子類初始化;
- 定義類的數組引用而不賦值,不會觸發此類的初始化;
- 訪問類定義的常量,不會觸發此類的初始化
三、三種類加載器#
- Bootstrap Classloader 是在 Java 虛擬機啟動後初始化的。
- Bootstrap Classloader 負責加載 ExtClassLoader,並且將 ExtClassLoader 的父加載器設置為 Bootstrap Classloader
- Bootstrap Classloader 加載完 ExtClassLoader 後,就會加載 AppClassLoader,並且將 AppClassLoader 的父加載器指定為 ExtClassLoader
Bootstrap ClassLoader#
啟動類加載器
:負責加載存放在 JDK\jre\lib (JDK 代表 JDK 的安裝目錄,下同) 下,或被 - Xbootclasspath 參數指定的路徑中的,並且能被虛擬機識別的類庫(如 rt.jar,所有的 java. 開頭的類均被 Bootstrap ClassLoader 加載)。啟動類加載器是無法被 Java 程序直接引用的。
Extension ClassLoader#
擴展類加載器
:該加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載 JDK\jre\lib\ext 目錄中,或者由 java.ext.dirs 系統變量指定的路徑中的所有類庫(如 javax. 開頭的類),開發者可以直接使用擴展類加載器。
Application ClassLoader#
應用程序類加載器
:該類加載器由 sun.misc.Launcher$AppClassLoader 來實現,它負責加載用戶類路徑(程序自己 classpath 下的類)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
類加載器的隔離問題#
每個類裝載器都有一個自己的命名空間用來保存已裝載的類。當一個類裝載器裝載一個類時,它會通過保存在命名空間裡的類全局限定名 (Fully Qualified Class Name) 進行搜索來檢測這個類是否已經被加載了。
JVM 及 Dalvik 對類唯一的識別是 ClassLoader id + PackageName + ClassName
,所以一個運行程序中是有可能存在兩個包名和類名完全一致的類的。並且如果這兩個類不是由一個 ClassLoader 加載,是無法將一個類的實例強轉為另外一個類的,這就是 ClassLoader 隔離性。
為了解決類加載器的隔離問題,JVM 引入了雙親委託機制
四、雙親委託模型#
核心思想:其一,自底向上檢查類是否已加載
;其二,自頂向下嘗試加載類
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類
具體加載過程#
- 當 AppClassLoader 加載一個 class 時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。
- 當 ExtClassLoader 加載一個 class 時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。
- 如果 BootStrapClassLoader 加載失敗(例如在 % JAVA_HOME%/jre/lib 裡未查找到該 class),會使用 ExtClassLoader 來嘗試加載;
- 如果 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,如果 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException
雙親委派模型意義#
- 系統類防止內存中出現多份同樣的字節碼,使得類有了層次的劃分
- 保證 Java 程序安全穩定運行
就拿java.lang.Object
來說,你加載它經過一層層委托最終是由 Bootstrap ClassLoader 來加載的,也就是最終都是由 Bootstrap ClassLoader 去找 <JAVA_HOME>\lib 中rt.jar
裡面的 java.lang.Object 加載到 JVM 中,這樣如果有不法分子自己造了個 java.lang.Object,裡面嵌了不好的代碼,但是如果按照雙親委派模型來實現類加載的話,最終加載到 JVM 中的只會是我們 rt.jar 裡面的東西,也就是這些核心的基礎類代碼得到了保護
五、類的加載方式#
- 命令行啟動應用時由 JVM 初始化加載
- 通過 Class.forName () 方法動態加載
- 通過 ClassLoader.loadClass () 方法動態加載
Class.forName () 和 ClassLoader.loadClass ()
- Class.forName ():把類的.class 文件加載到 JVM 中,對類進行解釋的同時執行類中的 static 靜態代碼塊;
- ClassLoader.loadClass ():只是把.class 文件加載到 JVM 中,不會執行 static 代碼塊中的內容,只有在 newInstance 才會去執行
六、自定義加載器#
應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因為 JVM 自帶的 ClassLoader 只是懂得從本地文件系統加載標準的 java class 文件,因此如果編寫了自己的 ClassLoader,便可以做到如下幾點:
- 在執行非信任代碼之前,自動驗證數字簽名。
- 動態地創建符合用戶特定需要的定制化構建類。
- 從特定的場所取得 java class,例如數據庫中和網絡中