類加載器簡(jiǎn)單來說是用來加載 Java 類到 Java 虛擬機(jī)中的罪帖。Java 虛擬機(jī)使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼邮屁,并轉(zhuǎn)換成 java.lang.Class類的一個(gè)實(shí)例整袁。每個(gè)這樣的實(shí)例用來表示一個(gè) Java 類。通過此實(shí)例的 newInstance()方法就可以創(chuàng)建出該類的一個(gè)對(duì)象佑吝。
想要真正深入理解Java類加載機(jī)制坐昙,就要弄懂三個(gè)問題:類什么時(shí)候加載、類加載的過程是什么芋忿、用什么加載炸客。所以本文分為三部分分別介紹Java類加載的時(shí)機(jī)疾棵、類加載的過程、加載器痹仙。
一是尔、Java類加載的時(shí)機(jī)
1.1 類加載的生命周期
類加載的生命周期是從類被加載到內(nèi)存開始,直到卸載處內(nèi)存為止的蝶溶。整個(gè)生命周期分為7個(gè)階段:加載嗜历、驗(yàn)證、準(zhǔn)備抖所、解析梨州、初始化、使用田轧、卸載暴匠。其中,驗(yàn)證傻粘、準(zhǔn)備每窖、解析三部分統(tǒng)稱為連接。具體步驟如下圖所示:
下面簡(jiǎn)單介紹下類加載器所執(zhí)行的生命周期的過程弦悉。
(1) 裝載:查找和導(dǎo)入Class文件窒典;
(2) 鏈接:把類的二進(jìn)制數(shù)據(jù)合并到JRE中;
(a)校驗(yàn):檢查載入Class文件數(shù)據(jù)的正確性稽莉;
(b)準(zhǔn)備:給類的靜態(tài)變量分配存儲(chǔ)空間瀑志;
(c)解析:將符號(hào)引用轉(zhuǎn)成直接引用;
(3) 初始化:對(duì)類的靜態(tài)變量污秆,靜態(tài)代碼塊執(zhí)行初始化操作劈猪。
1.2 類加載的時(shí)機(jī)
類加載的時(shí)機(jī)Java虛擬機(jī)規(guī)范中并沒有強(qiáng)制規(guī)定,但是對(duì)于初始化階段良拼,有5種場(chǎng)景必須立即執(zhí)行初始化战得,也被稱為主動(dòng)引用。
(1) 遇到new庸推、getstatic常侦、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化贬媒,則需要先觸發(fā)其初始化刮吧。生成這4條指令的最常見的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候,讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾掖蛤、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候井厌。
(2) 使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候蚓庭,如果類沒有進(jìn)行過初始化致讥,則需要先觸發(fā)其初始化。
(3) 當(dāng)初始化一個(gè)類的時(shí)候器赞,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化垢袱,則需要先觸發(fā)其父類的初始化。
(4)當(dāng)虛擬機(jī)啟動(dòng)時(shí)港柜,用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類)请契,虛擬機(jī)會(huì)先初始化這個(gè)主類。
(5)當(dāng)使用JDK 1.7動(dòng)態(tài)語言支持時(shí)夏醉,如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic爽锥、REF_putStatic、REF_invokeStatic的方法句柄畔柔,并且該方法句柄所對(duì)應(yīng)的類沒有初始化過氯夷,則先觸發(fā)初始化。
二靶擦、Java類加載的過程
類加載的全過程分為7個(gè)階段腮考,但是主要的過程是加載、驗(yàn)證玄捕、準(zhǔn)備踩蔚、解析、初始化這5個(gè)階段枚粘。
2.1 加載
在加載階段馅闽,虛擬機(jī)需要完成3件事情:
(1) 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流;
(2) 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)赌结;
(3) 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象捞蛋,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
2.2 驗(yàn)證
驗(yàn)證階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求柬姚,并且不會(huì)危害虛擬機(jī)自身的安全拟杉。整體來看,驗(yàn)證階段大致分為4個(gè)驗(yàn)證動(dòng)作量承。
(1)文件格式驗(yàn)證
第一階段是驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范搬设,并且能被當(dāng)前版本的虛擬機(jī)處理。主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi)撕捍,格式上符合描述一個(gè)Java類型信息的要求拿穴。該階段是基于二進(jìn)制字節(jié)流驗(yàn)證的,只有通過了這個(gè)階段的驗(yàn)證忧风,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法去中存儲(chǔ)默色,后面的3個(gè)驗(yàn)證都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。
這一階段可能的驗(yàn)證點(diǎn):
- 是否以魔數(shù)開頭狮腿;
- 主腿宰、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍內(nèi)呕诉;
- 常量池的常量數(shù)據(jù)類型是否被支持;
- 吃度。甩挫。。
(2)元數(shù)據(jù)驗(yàn)證
元數(shù)據(jù)驗(yàn)證是對(duì)字節(jié)碼描述信息進(jìn)行語義分析椿每,以保證其描述的信息符合Java語言規(guī)范的要求伊者。這個(gè)階段可能的驗(yàn)證點(diǎn):
- 是否有父類;
- 是否繼承了不被允許繼承的類间护;
- 如果該類不是抽象類亦渗,是否實(shí)現(xiàn)了其父類或接口要求實(shí)現(xiàn)的所有方法;
- 兑牡。央碟。。
(3)字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證的主要目的是通過數(shù)據(jù)流和控制流分析均函,確定程序語義的合法性和邏輯性亿虽。該階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事情苞也。這個(gè)階段可能的驗(yàn)證點(diǎn):
- 保證任何時(shí)候操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列的一致性洛勉;
- 跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上;
- 如迟。收毫。。
(4)符號(hào)引用驗(yàn)證
符號(hào)引用驗(yàn)證的主要目的是保證解析動(dòng)作能正常執(zhí)行殷勘,如果無法通過符號(hào)引用驗(yàn)證此再,則會(huì)拋出異常。這個(gè)階段可能的驗(yàn)證點(diǎn):
- 符號(hào)引用的類玲销、字段输拇、方法的訪問性(public、private等)是否可被當(dāng)前類訪問贤斜;
- 指定類是否存在符合方法的字段描述符策吠;
- 。瘩绒。猴抹。
2.3 準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配,需要說明的是:
這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中;這里所說的初始值“通常情況”是數(shù)據(jù)類型的零值锁荔,例如:
public static int value = 1;
value在準(zhǔn)備階段過后的初始值為0而不是1,而把value賦值的putstatic指令將在初始化階段才會(huì)被執(zhí)行蟀给。
特殊情況:
public static final int value = 1;//此時(shí)準(zhǔn)備階段value賦值為1
2.4 解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換成直接引用的過程。直接引用是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄坤溃。直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存有關(guān)拍霜,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用不盡相同。
2.5 初始化
初始化階段是類加載過程的最后一步薪介,到了該階段才真正開始執(zhí)行類定義的Java程序代碼,根據(jù)程序員通過代碼定制的主觀計(jì)劃去初始化類變量和其他資源越驻,是執(zhí)行類構(gòu)造器初始化方法的過程汁政。
三、類加載器
類加載器大致可以分為以下3部分:
(1) 啟動(dòng)類加載器: 將存放于<JAVA_HOME>\lib目錄中的缀旁,或者被-Xbootclasspath參數(shù)所指定的路徑中的记劈,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如 rt.jar 名字不符合的類庫(kù)即使放在lib目錄中也不會(huì)被加載)類庫(kù)加載到虛擬機(jī)內(nèi)存中并巍。啟動(dòng)類加載器無法被Java程序直接引用目木。
(2) 擴(kuò)展類加載器 : 將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫(kù)加載懊渡。開發(fā)者可以直接使用擴(kuò)展類加載器刽射。
(3) 應(yīng)用程序類加載器: 負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù),開發(fā)者可直接使用。
我們的應(yīng)用程序都是由這三種類加載器相互配合加載的剃执。它們的關(guān)系如下圖所示誓禁,稱之為雙親委派模型。
工作過程:如果一個(gè)類加載器接收到了類加載的請(qǐng)求肾档,它首先把這個(gè)請(qǐng)求委托給他的父類加載器去完成摹恰,每個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求都應(yīng)該傳送到頂層的啟動(dòng)類加載器中怒见,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它在搜索范圍中沒有找到所需的類)時(shí)俗慈,子加載器才會(huì)嘗試自己去加載。
好處:java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系遣耍。例如類java.lang.Object闺阱,它存放在rt.jar中,無論哪個(gè)類加載器要加載這個(gè)類配阵,最終都會(huì)委派給啟動(dòng)類加載器進(jìn)行加載馏颂,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。相反棋傍,如果用戶自己寫了一個(gè)名為java.lang.Object的類救拉,并放在程序的Classpath中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類瘫拣,java類型體系中最基礎(chǔ)的行為也無法保證亿絮,應(yīng)用程序也會(huì)變得一片混亂。
雙親委派模型實(shí)現(xiàn)起來其實(shí)很簡(jiǎn)單,以下是實(shí)現(xiàn)代碼派昧,通過以下代碼黔姜,可以對(duì)JVM采用的雙親委派類加載機(jī)制有了更感性的認(rèn)識(shí)。
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protectedsynchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判斷該類型是否已經(jīng)被加載
Class c = findLoadedClass(name);
if (c == null) {
//如果沒有被加載蒂萎,就委托給父類加載或者委派給啟動(dòng)類加載器加載
try {
if (parent != null) {
//如果存在父類加載器秆吵,就委派給父類加載器加載
c = parent.loadClass(name, false);
} else {
//如果不存在父類加載器,就檢查是否是由啟動(dòng)類加載器加載的類五慈,通過調(diào)用本地方法
native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器和啟動(dòng)類加載器都不能完成加載任務(wù)纳寂,才調(diào)用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}