前言
本文已經(jīng)收錄到我的Github個(gè)人博客,歡迎大佬們光臨寒舍:
學(xué)習(xí)導(dǎo)圖

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

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

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