《深入理解 Java 虛擬機》學習 -- 類加載機制
1. 概述
虛擬機把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存乓搬,并對數(shù)據(jù)進行校驗思犁、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型进肯,這就是虛擬機的類加載機制激蹲。
2. 類加載的時機
2.1 類的生命周期:
加載 --> 連接(驗證 --> 準備 --> 解析)--> 初始化 --> 使用 --> 卸載
其中,加載江掩、驗證学辱、準備、初始化和卸載這五個階段的順序是確定的环形,類的加載過程必須按照這種順序按部就班地開始策泣,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 語言的運行時綁定抬吟。
2.2 類的初始化情況
有且只有四種情況必須立即對類進行 “初始化”:
-
遇到 new萨咕、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時拗军,如果類沒有進行過初始化任洞,則需要先觸發(fā)其初始化
場景:
- 使用 new 關(guān)鍵字實例化對象
- 讀取或設(shè)置一個類的靜態(tài)字段(被 final 修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)
- 調(diào)用一個類的靜態(tài)方法的時候
使用
java.lang.reflect
包的方法對類進行反射調(diào)用時发侵,如果類沒有進行過初始化交掏,則需要先觸發(fā)其初始化當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化刃鳄,則需要先觸發(fā)其父類的初始化
當虛擬機啟動時盅弛,用戶需要指定一個要執(zhí)行的主類(包含
main()
方法的那個類),虛擬機會先初始化這個主類
3. 類加載的過程
3.1 加載
在加載階段叔锐,虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在 Java 堆中生成一個代表這個類的
java.lang.Class
對象挪鹏,作為方法區(qū)這些數(shù)據(jù)的訪問入口
3.2 驗證
會完成四個階段的檢驗過程:
- 文件格式驗證
- 元數(shù)據(jù)驗證
- 字節(jié)碼驗證
- 符號引用驗證
3.3 準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進行分配愉烙。
幾個注意點:
這時候進行內(nèi)存分配的僅包括類變量(被 static 修飾的變量)讨盒,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在 Java 堆中(初始化階段)步责。
-
這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值(如0返顺、0L禀苦、null、false等)遂鹊,而不是被在Java代碼中被顯式地賦予的值振乏。
舉個例子:
// 變量 value 在準備階段過后的初始值為 0 而不是 123 public static int value = 123;
-
特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性(即 final 常量),那在準備階段變量 value 就會被初始化為 ConstantValue 屬性所指定的值秉扑。
舉個例子:
// 編譯時 Javac 將會為 value 生成 ConstantValue 屬性慧邮,在準備階段虛擬機就會根據(jù) ConstantValue 的設(shè)置將 value 賦值為 123 public static final int value = 123;
3.4 解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
概念說明:
- 符號引用:符號引用以一組符號來描述所引用的目標舟陆,符號可以是任何形式的字面量误澳,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)吨娜,引用的目標并不一定已經(jīng)加載到內(nèi)存中脓匿。
- 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄宦赠。直接引用是與虛擬機實現(xiàn)地內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同米母。如果有了直接引用勾扭,那引用的目標必定已經(jīng)在內(nèi)存中存在。
3.5 初始化
類初始化階段是類加載過程的最后一步铁瞒,前面的類加載過程(準備階段)中妙色,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制慧耍。到了初始化階段身辨,才真正開始執(zhí)行類中定義的 Java 程序代碼。
初始化階段是執(zhí)行類構(gòu)造器 <clinit>()
方法的過程:
在準備階段芍碧,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值煌珊,而在初始化階段,則是根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源(如類成員變量 -- 除了類變量以外的變量都屬于類成員變量)泌豆。
幾個應(yīng)該記住的概念:
<clinit>()
方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}
塊)中的語句合并產(chǎn)生的(會優(yōu)先執(zhí)行 static{})虛擬機會保證在子類的
<clinit>()
方法執(zhí)行之前定庵,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢。因此在虛擬機中第一個被執(zhí)行的<clinit>()
方法的類肯定是java.lang.Object
踪危。-
由于父類的
<clinit>()
方法先執(zhí)行蔬浙,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。舉個例子贞远,下面的代碼中運行結(jié)果 字段 B 的值將會是 2 而不是 1畴博。
static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }
<clinit>()
方法對于類或接口來說并不是必須的,如果一個類中沒有靜態(tài)語句塊蓝仲,也沒有對變量的賦值操作俱病,那么編譯器可以不為這個類生成<clinit>()
方法蜜唾。接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作庶艾,因此接口與類一樣都會生成
<clinit>()
方法袁余。
4. 類加載器
4.1. 比較兩個類是否 "相等"
只有在這兩個類是由同一個類加載器加載的前提之下才有意義,否則咱揍,即使這兩個類是來源于同一個 Class 文件颖榜,只要它們的類加載器不同,那這兩個類就必定不相等煤裙。
4.2 類加載器
不同的類加載器(Java 虛擬機角度)
- 啟動類加載器(C++ 實現(xiàn)):虛擬機自身一部分
- 所有其他的類加載器(Java 實現(xiàn)):獨立于虛擬機外部掩完,繼承自抽象類
java.lang.ClassLoader
不同的類加載器(Java 開發(fā)人員角度)
-
啟動類加載器:與上述一致
負責將存放在
<JAVA_HOME>\lib
目錄中的,或者被-Xbootclasspath
參數(shù)所指定的路徑中硼砰,并且是虛擬機識別的類庫加載到虛擬機內(nèi)存中且蓬。啟動類加載器無法被 Java 程序直接引用。 -
擴展類加載器:由
sun.misc.Launcher$ExtClassLoader
實現(xiàn)負責加載
<JAVA_HOME>\lin\ext
目錄中的题翰,或者被java.ext.dirs
系統(tǒng)變量所指定的路徑中的所有類庫恶阴,開發(fā)者可以直接使用擴展類加載器 -
應(yīng)用程序類加載器:由
sun.misc.Launcher$AppClassLoader
來實現(xiàn)由于這個類加載器是
ClassLoader
中的getSystemClassLoader()
方法的返回值,所以一般也稱它為系統(tǒng)類加載器豹障。它負責加載用戶路徑(ClassPath)上所指定的類庫冯事,開發(fā)者可以直接使用這個類加載器,如果應(yīng)該程序中沒有自定義過自己的類加載器血公,一般情況下這個就是程序中默認的類加載器昵仅。
4.3 雙親委派模型(重要)
上圖:
結(jié)構(gòu):
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當有自己的父類加載器累魔。這里類加載器之間的父子關(guān)系一般不會以繼承的關(guān)系來實現(xiàn)摔笤,而是都使用組合關(guān)系來復用父加載器的代碼。
工作過程:
如果一個類加載器收到了類加載的請求垦写,它首先不會自己先嘗試加載這個類吕世,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此梯澜,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中寞冯,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載晚伙。
實現(xiàn)原理
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先吮龄,檢查請求的類是否已經(jīng)被加載過了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNUll(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出 ClassNotFoundException
// 則說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載的時候
// 再調(diào)用本身的 findClass 方法進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父加載器的 loadClass()
方法咆疗,若父加載器為空則默認使用啟動類加載器作為父加載器漓帚。如果父類加載失敗,則在拋出 ClassNotFoundException
異常后午磁,再調(diào)用自己的 findClass()
方法進行加載尝抖。
Java 中不符合雙親委派模型的例子
-
基礎(chǔ)類需要調(diào)用回用戶的代碼
引用上下文類加載器(Thread Context ClassLoader)毡们,這個類加載器可以通過
java.lang.Thread
類的setContextClassLoaser()
方法進行設(shè)置,如果創(chuàng)建線程時還未設(shè)置昧辽,它將會從父線程中繼承一個衙熔;如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,那么這個類加載器默認就是應(yīng)用程序類加載器搅荞。因為父類加載器請求子類加載器去完成類加載的動作红氯,實際上是逆向使用類加載器,不符合雙親委派模型咕痛。
如:JDBC
-
程序動態(tài)性
如代碼熱替換痢甘,模塊熱部署等。(其實就是模塊化操作)
這種模塊化的模式標準我們稱之為 "OSGi"茉贡。(自定義的類加載器機制的實現(xiàn))
在該標準下塞栅,每一個程序模塊(Bundle)都有一個自己的類加載器,當需要更換一個 Bundle 時腔丧,就把 Bundle 連同類加載器一起換掉以實現(xiàn)代碼的熱替換放椰。
在 OSGi 環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)果悔据,而是進一步發(fā)展為網(wǎng)狀結(jié)構(gòu)庄敛。