前言
本文已經收錄到我的Github個人博客,歡迎大佬們光臨寒舍:
學習導圖

一.為什么要學習類加載機制?
今天想跟大家嘮嗑嘮嗑Java的類加載機制,這是Java的一個很重要的創新點,曾經也是Java流行的重要原因之一。
Oracle當初引入這個機制是為了滿足Java Applet開發的需求,JVM咬咬牙引入了Java類加載機制,后來的基於Jvm的動態部署,插件化開發包括大家熱議的熱修復,總之很多后來的技術都源於在JVM中引入了類加載器。
如今,類加載機制也在各個領域大放異彩,在面試中,由類加載機制所衍生出來各類面試題也層出不窮。
所以,我們要了解下類加載機制,為工作中或者是面試中實際的需要打好良好的基礎。
二.核心知識點歸納
2.1 概述
Q1:JVM類加載機制定義:
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可被虛擬機直接使用的Java類型的過程
Q2:特性
運行期類加載。即在Java語言里面,類型的加載、連接和初始化過程都是在程序運行期完成的,從而通過犧牲一些性能開銷來換取Java程序的高度靈活性
什么是運行期,什么是編譯期?
- 編譯期是指編譯器將源代碼翻譯為機器能識別的代碼,
Java被編譯為Jvm認識的字節碼文件- 運行期則是指
Java代碼的運行過程
JVM運行期動態加載+動態連接->Java的動態擴展特性
2.2 類加載的過程
類從被加載到虛擬機內存中開始、到卸載出內存為止,整個生命周期包括七個階段:
-
加載
-
驗證
-
准備
-
解析
-
初始化
-
使用
-
卸載
其中,驗證、准備、解析這3個部分統稱為連接,流程如下圖:

注意:
- 『加載』->『驗證』->『准備』->『初始化』->『卸載』這五個階段的順序是確定的,而『解析』可能為了支持
Java的動態綁定會在『初始化』后才開始- 上述階段通常都是互相交叉地混合式進行的,比如會在一個階段執行的過程中調用、激活另外一個階段
想要了解Java動態綁定和靜態綁定區別的話,可以看下這篇文章:理解靜態綁定與動態綁定
2.2.1 加載
Q1:任務
- 通過類的全限定名來獲取定義此類的二進制字節流。如從
ZIP包讀取、從網絡中獲取、通過運行時計算生成、由其他文件生成、從數據庫中讀取等等途徑......
想要詳細了解類的全限定名的知識,可以看下這篇文章:全限定名、簡單名稱和描述符是什么東西?
- 將該二進制字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,該數據存儲數據結構由虛擬機實現自行定義
- 在內存中生成一個代表這個類的
java.lang.Class對象,它將作為程序訪問方法區中的這些類型數據的外部接口
2.2.2 驗證
- 是連接階段的第一步,且工作量在
JVM類加載子系統中占了相當大的一部分 - 目的:為了確保
Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全
由此可見,它能直接決定
JVM能否承受惡意代碼的攻擊,因此驗證階段很重要,但由於它對程序運行期沒有影響,並不一定必要,可以考慮使用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
-
檢驗過程包括下面四個階段:
A.文件格式驗證:
-
內容:驗證字節流是否符合
Class文件格式的規范、以及是否能被當前版本的虛擬機處理 -
目的:保證輸入的字節流能正確地解析並存儲於方法區之內,且格式上符合描述一個
Java類型信息的要求。只有保證二進制字節流通過了該驗證后,它才會進入內存的方法區中進行存儲,所以后續3個驗證階段全部是基於方法區而不是字節流了 -
例子:
-
是否以魔數
0xCAFEBABE開頭 -
主次版本號是否在
JVM接受范圍內 -
索引值是否有指向不存在/不符合類型的常量
......
-
B.元數據驗證:
-
內容:對字節碼描述的信息進行語義分析,以保證其描述的信息符合
Java語言規范的要求 -
目的:對類的元數據信息進行語義校驗,保證不存在不符合
Java語言規范的元數據信息 -
例子:
-
類是否有父類(除了
java.lang.Object之外,所有類都應有父類) -
父類是否繼承了不允許被繼承的類(
final修飾的類) -
如果該類不是抽象類,是否實現了其父類或接口中要求實現的所有方法
......
-
C.字節碼驗證:
-
是驗證過程中最復雜的一個階段
-
內容:對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件
-
目的:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的
-
例子:
-
保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現“在操作數棧的數據類型中放置了
int類型的數據,使用時卻按long類型來載入本地變量表中” -
保證任何跳轉指令都不會跳轉到方法體外的字節碼指令上
......
-
D.符號引用驗證:
- 內容:對類自身以外(如常量池中的各種符號引用)的信息進行匹配性校驗
- 目的:確保解析動作能正常執行,如果無法通過符號引用驗證,那么將會拋出一個
java.lang.IncompatibleClassChangeError異常的子類 - 注意:該驗證發生在虛擬機將符號引用轉化為直接引用的時候,即『解析』階段
-
2.2.3 准備
Q1:任務
- 為類變量(靜態變量)分配內存:因為這里的變量是由方法區分配內存的,所以僅包括類變量而不包括實例變量,后者將會在對象實例化時隨着對象一起分配在
Java堆中 - 設置類變量初始值:通常情況下零值
2.2.4 解析
之前提過,解析階段就是虛擬機將常量池內的符號引用替換為直接引用的過程
- 符號引用:以一組符號來描述所引用的目標
- 可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可
- 與虛擬機實現的內存布局無關,因為符號引用的字面量形式明確定義在
Java虛擬機規范的Class文件格式中,所以即使各種虛擬機實現的內存布局不同,但是能接受符號引用都是一致的
- 直接引用:
- 可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄
- 與虛擬機實現的內存布局相關,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不同
- 發生時間:
JVM會根據需要來判斷,是在類被加載器加載時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才去解析 - 解析動作:有七類符號及其對應在常量池的七種常量類型
- 類或接口(
CONSTANT_Class_info)- 字段(
CONSTANT_Fieldref_info)- 類方法(
CONSTANT_Methodref_info)- 接口方法(
CONSTANT_InterfaceMethodref_info)- 方法類型(
CONSTANT_MethodType_info)- 方法句柄(
CONSTANT_MethodHandle_info)- 調用點限定符(
CONSTANT_InvokeDynamic_info)
舉個例子,設當前代碼所處的為類D,把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,解析過程分三步:
- 若
C不是數組類型:JVM將會把代表N的全限定名傳遞給D類加載器去加載這個類C。在加載過程中,由於元數據驗證、字節碼驗證的需要,又可能觸發其他相關類的加載動作。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。- 若
C是數組類型且數組元素類型為對象:JVM也會按照上述規則加載數組元素類型- 若上述步驟無任何異常:此時
C在JVM中已成為一個有效的類或接口,但在解析完成前還需進行符號引用驗證,來確認D是否具備對C的訪問權限。如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError異常
Q1:字段(成員變量/域)和屬性有什么區別?
- 屬性,是指對象的屬性,對於
JavaBean來說,是getXXX方法定義的- 字段,是成員變量
class Person{
private String mingzi; //mingzi是字段,一般來說字段和屬性是相同的,但是這個例子是特例
public String getName(){ //name是屬性
return mingzi:
}
public void setName(){
mingzi= "張三";
}
}
2.2.5 初始化
- 是類加載過程的最后一步,會開始真正執行類中定義的
Java代碼。而之前的類加載過程中,除了在『加載』階段用戶應用程序可通過自定義類加載器參與之外,其余階段均由虛擬機主導和控制 - 與『准備』階段的區分:
- 准備階段:變量賦初始零值
- 初始化階段:根據Java程序的設定去初始化類變量和其他資源,或者說是執行類構造器
clinit的過程
clinit:由編譯器自動收集類中的所有類變量(靜態變量)的賦值動作和靜態語句塊static{}中的語句合並產生
- 是線程安全的,在多線程環境中被正確地加鎖、同步
- 對於類或接口來說是非必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成
clinit- 接口與類不同的是,執行接口的
clinit不需要先執行父接口的clinit,只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的clinit
想詳細了解clinit以及其與init的區別的讀者,可以看下這篇文章:深入理解jvm--Java中init和clinit區別完全解析
- 在虛擬機規范中,規定了有且只有五種情況必須立即對類進行『初始化』:
- 遇到
new、getstatic、putstatic或invokestatic這4條字節碼指令時- 使用
java.lang.reflect包的方法對類進行反射調用的時候- 當初始化一個類的時候,若發現其父類還未進行初始化,需先觸發其父類的初始化
- 在虛擬機啟動時,需指定一個要執行的主類,虛擬機會先初始化它
- 當使用
JDK1.7的動態語言支持時,若一個java.lang.invoke.MethodHandle實例最后的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且這個方法句柄所對應的類未進行初始化,需先觸發其初始化。
2.3 類加載器&雙親委派模型
每個類加載器,都擁有一個獨立的命名空間,它不僅用於加載類,還和這個類本身一起作為在
JVM中的唯一標識。所以比較兩個類是否相等,只要看它們是否由同一個類加載器加載,即使它們來源於同一個Class文件且被同一個JVM加載,只要加載它們的類加載器不同,這兩個類就必定不相等
2.3.1 類加載器
從JVM的角度,可將類加載器分為兩種:
- 啟動類加載器
- 由
C++語言實現,是虛擬機自身的一部分- 負責加載存放在
<JAVA_HOME>\lib目錄中、或被-Xbootclasspath參數所指定路徑中的、且可被虛擬機識別的類庫- 無法被
Java程序直接引用,如果自定義類加載器想要把加載請求委派給引導類加載器的話,可直接用null代替
- 其他類加載器:由
Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader,可被Java程序直接引用。常見幾種:
擴展類加載器
A.由
sun.misc.Launcher$ExtClassLoader實現B.負責加載
<JAVA_HOME>\lib\ext目錄中的、或者被java.ext.dirs系統變量所指定的路徑中的所有類庫應用程序類加載器
A.是默認的類加載器,是
ClassLoader#getSystemClassLoader()的返回值,故又稱為系統類加載器B.由
sun.misc.Launcher$App-ClassLoader實現C.負責加載用戶類路徑上所指定的類庫
自定義類加載器:如果以上類加載起不能滿足需求,可自定義

需要注意的是:雖然數組類不通過類加載器創建而是由
JVM直接創建的,但仍與類加載器有密切關系,因為數組類的元素類型最終還要靠類加載器去創建
2.3.2 雙親委派模型
- 定義:表示類加載器之間的層次關系
- 前提:除了頂層啟動類加載器外,其余類加載器都應當有自己的父類加載器,且它們之間關系一般不會以繼承關系來實現,而是通過組合關系來復用父加載器的代碼
- 工作過程:若一個類加載器收到了類加載的請求,它先會把這個請求委派給父類加載器,並向上傳遞,最終請求都傳送到頂層的啟動類加載器中。只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載
- 注意:不是一個強制性的約束模型,而是
Java設計者推薦給開發者的一種類加載器實現方式 - 優點:類會隨着它的類加載器一起具備帶有優先級的層次關系,可保證
Java程序的穩定運作;實現簡單,所有實現代碼都集中在java.lang.ClassLoader的loadClass()中
比如,某些類加載器要加載
java.lang.Object類,最終都會委派給最頂端的啟動類加載器去加載,這樣Object類在程序的各種類加載器環境中都是同一個類。相反,系統中將會出現多個不同的
Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂
三.課堂小測試
恭喜你!已經看完了前面的文章,相信你對
JVM類加載機制已經有一定深度的了解,下面,進行一下課堂小測試,驗證一下自己的學習成果吧!
Q1:類加載的全過程是怎樣的?
Q2:什么是雙親委派模型?
Q3:String類如何被加載的
上面問題的答案,在前文都提到過,如果還不能回答出來的話,建議回顧下前文
Q4:請你談談類加載過程,以Person a = new Person();為例進行說明
這道題是在牛客的暑假實習
Tencent一面的面筋上找的,附上標准答案:類的加載過程,Person person = new Person();為例進行說明
如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力
本文參考鏈接:
