前言
本文已經(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)存月幌,并對(duì)數(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í)椒惨,可以看下這篇文章:全限定名、簡(jiǎn)單名稱和描述符是什么東西潮罪?
- 將該二進(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
對(duì)象,它將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口
2.2.2 驗(yàn)證
- 是連接階段的第一步嫉到,且工作量在
JVM
類加載子系統(tǒng)中占了相當(dāng)大的一部分 - 目的:為了確保
Class
文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求沃暗,并且不會(huì)危害虛擬機(jī)自身的安全
由此可見,它能直接決定
JVM
能否承受惡意代碼的攻擊何恶,因此驗(yàn)證階段很重要孽锥,但由于它對(duì)程序運(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
開頭主次版本號(hào)是否在
JVM
接受范圍內(nèi)-
索引值是否有指向不存在/不符合類型的常量
......
B.元數(shù)據(jù)驗(yàn)證:
內(nèi)容:對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合
Java
語言規(guī)范的要求目的:對(duì)類的元數(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)容:對(duì)類的方法體進(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.符號(hào)引用驗(yàn)證:
- 內(nèi)容:對(duì)類自身以外(如常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)
- 目的:確保解析動(dòng)作能正常執(zhí)行总滩,如果無法通過符號(hào)引用驗(yàn)證,那么將會(huì)拋出一個(gè)
java.lang.IncompatibleClassChangeError
異常的子類 - 注意:該驗(yàn)證發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候巡雨,即『解析』階段
2.2.3 準(zhǔn)備
Q1:任務(wù)
- 為類變量(靜態(tài)變量)分配內(nèi)存:因?yàn)檫@里的變量是由方法區(qū)分配內(nèi)存的闰渔,所以僅包括類變量而不包括實(shí)例變量,后者將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在
Java
堆中 - 設(shè)置類變量初始值:通常情況下零值
2.2.4 解析
之前提過铐望,解析階段就是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程
- 符號(hào)引用:以一組符號(hào)來描述所引用的目標(biāo)
- 可以是任何形式的字面量冈涧,只要使用時(shí)能無歧義地定位到目標(biāo)即可
- 與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),因?yàn)榉?hào)引用的字面量形式明確定義在
Java
虛擬機(jī)規(guī)范的Class
文件格式中正蛙,所以即使各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局不同督弓,但是能接受符號(hào)引用都是一致的
- 直接引用:
- 可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄
- 與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)跟畅,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不同
- 發(fā)生時(shí)間:
JVM
會(huì)根據(jù)需要來判斷咽筋,是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析溶推,還是等到一個(gè)符號(hào)引用將要被使用前才去解析 - 解析動(dòng)作:有七類符號(hào)及其對(duì)應(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è)從未解析過的符號(hào)引用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ù)組元素類型為對(duì)象:JVM
也會(huì)按照上述規(guī)則加載數(shù)組元素類型- 若上述步驟無任何異常:此時(shí)
C
在JVM
中已成為一個(gè)有效的類或接口,但在解析完成前還需進(jìn)行符號(hào)引用驗(yàn)證赘风,來確認(rèn)D
是否具備對(duì)C
的訪問權(quán)限夹囚。如果發(fā)現(xiàn)不具備訪問權(quán)限,將拋出java.lang.IllegalAccessError
異常
Q1:字段(成員變量/域)和屬性有什么區(qū)別邀窃?
- 屬性荸哟,是指對(duì)象的屬性,對(duì)于
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)境中被正確地加鎖仅讽、同步
- 對(duì)于類或接口來說是非必需的陶缺,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對(duì)變量的賦值操作洁灵,那么編譯器可以不為這個(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ī)定了有且只有五種情況必須立即對(duì)類進(jìn)行『初始化』:
- 遇到
new
、getstatic
牍汹、putstatic
或invokestatic
這4條字節(jié)碼指令時(shí)- 使用
java.lang.reflect
包的方法對(duì)類進(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è)方法句柄所對(duì)應(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
程序直接引用,如果自定義類加載器想要把加載請(qǐng)求委派給引導(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è)類加載器收到了類加載的請(qǐng)求痹筛,它先會(huì)把這個(gè)請(qǐng)求委派給父類加載器,并向上傳遞廓鞠,最終請(qǐng)求都傳送到頂層的啟動(dòng)類加載器中帚稠。只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求時(shí),子加載器才會(huì)嘗試自己去加載
-
注意:不是一個(gè)強(qiáng)制性的約束模型诫惭,而是
Java
設(shè)計(jì)者推薦給開發(fā)者的一種類加載器實(shí)現(xiàn)方式 -
優(yōu)點(diǎn):類會(huì)隨著它的類加載器一起具備帶有優(yōu)先級(jí)的層次關(guān)系翁锡,可保證
Java
程序的穩(wěn)定運(yùn)作蔓挖;實(shí)現(xiàn)簡(jiǎ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ì)變得一片混亂
三.課堂小測(cè)試
恭喜你匆瓜!已經(jīng)看完了前面的文章赢笨,相信你對(duì)
JVM
類加載機(jī)制已經(jīng)有一定深度的了解,下面驮吱,進(jìn)行一下課堂小測(cè)試茧妒,驗(yàn)證一下自己的學(xué)習(xí)成果吧!
Q1:類加載的全過程是怎樣的左冬?
Q2:什么是雙親委派模型桐筏?
Q3:String
類如何被加載的
上面問題的答案,在前文都提到過拇砰,如果還不能回答出來的話梅忌,建議回顧下前文
Q4:請(qǐng)你談?wù)勵(lì)惣虞d過程狰腌,以Person a = new Person();
為例進(jìn)行說明
這道題是在拍恋客的暑假實(shí)習(xí)
Tencent
一面的面筋上找的琼腔,附上標(biāo)準(zhǔn)答案:類的加載過程,Person person = new Person();為例進(jìn)行說明
如果文章對(duì)您有一點(diǎn)幫助的話踱葛,希望您能點(diǎn)一下贊展姐,您的點(diǎn)贊,是我前進(jìn)的動(dòng)力
本文參考鏈接: