類裝載器子系統(tǒng)是JVM中非常重要的部分筛武,是學(xué)習(xí)JVM繞不開的一關(guān)。
一般來說委刘,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ì)象。
類的生命周期#
我們先來看下類的生命周期绷柒,包括:
- 加載
- 連接
- 初始化
- 使用
- 卸載
其中加載志于、連接、初始化屬于類加載過程废睦。
使用是指我們new對(duì)象進(jìn)行使用伺绽。
卸載指對(duì)象被GC垃圾回收掉。
類加載過程#
JVM的類加載的過程是通過引導(dǎo)類加載器(bootstrap class loader)創(chuàng)建一個(gè)初始類(initial class)來完成的,這個(gè)類是由JVM的具體實(shí)現(xiàn)指定的憔恳。
Class 文件需要加載到虛擬機(jī)中之后才能運(yùn)行和使用瓤荔,系統(tǒng)加載 Class 類型的文件份如下幾步:
- 加載
- 連接
- 驗(yàn)證
- 準(zhǔn)備
- 解析
- 初始
順序是這樣一個(gè)順序,但是加載階段和連接階段的部分內(nèi)容是交叉進(jìn)行的钥组,加載階段尚未結(jié)束输硝,連接階段可能就已經(jīng)開始了。
下面我們來逐步解析
加載#
這里的加載是微觀上的程梦,是類加載過程中的一小步点把,也是第一步,類加載過程中的加載是宏觀上的屿附。
加載的流程如下:
- 通過全類名獲取定義此類的二進(jìn)制字節(jié)流
- 將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表該類的
Class
對(duì)象郎逃,作為方法區(qū)這些數(shù)據(jù)的訪問入口
簡(jiǎn)單來說就是:加載二進(jìn)制數(shù)據(jù)到內(nèi)存 —> 映射成JVM能識(shí)別的結(jié)構(gòu)—> 在內(nèi)存中生成class文件。
在虛擬機(jī)規(guī)范上挺份,對(duì)這部分的規(guī)定并不具體褒翰,所以實(shí)現(xiàn)方式是很靈活的。
加載階段我們可以用自定義類加載器去控制字節(jié)流的獲取方式匀泊,是非數(shù)組類的可控性最強(qiáng)的階段优训,而數(shù)組類型不通過類加載器創(chuàng)建,它由 Java 虛擬機(jī)直接創(chuàng)建各聘。
關(guān)于類加載器是什么揣非,后文再聊。
連接#
連接分為三步躲因,驗(yàn)證早敬、準(zhǔn)備、解析大脉,目的是將上面創(chuàng)建好的Class類合并至JVM中搞监,使之能夠執(zhí)行的過程。
驗(yàn)證#
確保class文件中的字節(jié)流包含的信息镰矿,符合當(dāng)前虛擬機(jī)的要求腺逛,保證這個(gè)被加載的class類的正確性,不會(huì)危害到虛擬機(jī)的安全衡怀。
準(zhǔn)備#
為類中的靜態(tài)字段分配內(nèi)存,并設(shè)置默認(rèn)的初始值安疗,比如int類型初始值是0抛杨。
被final修飾的static字段不會(huì)設(shè)置,因?yàn)閒inal在編譯的時(shí)候就分配了荐类。
解析#
解析階段的目的怖现,是將常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用的過程。
解析動(dòng)作主要針對(duì)類、接口屈嗤、字段潘拨、類方法、接口方法饶号、方法類型等铁追。
如果符號(hào)引用指向一個(gè)未被加載的類,或者未被加載類的字段或方法茫船,那么解析將觸發(fā)這個(gè)類的加載(但未必觸發(fā)這個(gè)類的鏈接以及初始化琅束。)
符號(hào)引用就是一組符號(hào)來描述目標(biāo),可以是任何字面量算谈,符號(hào)引用的字面量形式明確定在《Java 虛擬機(jī)規(guī)范》的Class文件格式中涩禀。
直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄然眼。
舉個(gè)例子:
在程序執(zhí)行方法時(shí)艾船,系統(tǒng)需要明確知道這個(gè)方法所在的位置。
Java 虛擬機(jī)為每個(gè)類都準(zhǔn)備了一張方法表來存放類中所有的方法高每。
當(dāng)需要調(diào)用一個(gè)類的方法的時(shí)候屿岂,只要知道這個(gè)方法在方法表中的偏移量就可以直接調(diào)用該方法了。
通過解析操作符號(hào)引用就可以直接轉(zhuǎn)變?yōu)?strong>目標(biāo)方法在類中方法表的位置觉义,從而使得方法可以被調(diào)用雁社。
所以,解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程晒骇,也就是得到類或者字段霉撵、方法在內(nèi)存中的指針或者偏移量。
初始化#
初始化就是執(zhí)行類的構(gòu)造器方法洪囤,是類加載的最后一步徒坡,這一步 JVM才開始真正執(zhí)行類中定義的 Java 程序代碼
這個(gè)方法不需要定義,是javac編譯器自動(dòng)收集類中所有類變量的賦值動(dòng)作和靜態(tài)代碼塊中的語句合并來的瘤缩。
若該類具有父類喇完,jvm
會(huì)保證父類的init()
先執(zhí)行,然后在執(zhí)行子類的init()
剥啤。
對(duì)于初始化階段锦溪,虛擬機(jī)嚴(yán)格規(guī)范了有且只有 5 種情況下,必須對(duì)類進(jìn)行初始化府怯,只有主動(dòng)去使用類才會(huì)初始化類:
-
當(dāng)遇到
new
刻诊、getstatic
、putstatic
或invokestatic
這 4 條直接碼指令時(shí)- 當(dāng)遇到一個(gè)類牺丙,讀取一個(gè)靜態(tài)字段(未被 final 修飾)则涯、或調(diào)用一個(gè)類的靜態(tài)方法時(shí)复局。
- 當(dāng) JVM執(zhí)行
new
指令時(shí)會(huì)初始化類。即當(dāng)程序創(chuàng)建一個(gè)類的實(shí)例對(duì)象粟判。 - 當(dāng) JVM執(zhí)行
getstatic
指令時(shí)會(huì)初始化類亿昏。即程序訪問類的靜態(tài)變量(不是靜態(tài)常量,常量會(huì)被加載到運(yùn)行時(shí)常量池)档礁。 - 當(dāng) JVM執(zhí)行
putstatic
指令時(shí)會(huì)初始化類角钩。即程序給類的靜態(tài)變量賦值。 - 當(dāng) JVM執(zhí)行
invokestatic
指令時(shí)會(huì)初始化類事秀。即程序調(diào)用類的靜態(tài)方法彤断。
對(duì)類進(jìn)行反射調(diào)用時(shí),如果類沒初始化易迹,需要觸發(fā)其初始化宰衙。
初始化一個(gè)類,如果其父類還未初始化睹欲,則先觸發(fā)該父類的初始化供炼。
當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要定義一個(gè)要執(zhí)行的主類 (包含 main 方法的那個(gè)類)窘疮,虛擬機(jī)會(huì)先初始化這個(gè)類袋哼。
MethodHandle
和VarHandle
可以看作是輕量級(jí)的反射調(diào)用機(jī)制,而要想使用這 2 個(gè)調(diào)用闸衫, 就必須先使用findStaticVarHandle
來初始化要調(diào)用的類涛贯。「補(bǔ)充,來自issue745」 當(dāng)一個(gè)接口中定義了 JDK8 新加入的默認(rèn)方法(被 default 關(guān)鍵字修飾的接口方法)時(shí)蔚出,如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化弟翘,那該接口要在其之前被初始化。
類加載器#
三大類加載器#
了解了類加載過程后骄酗,我們來看看類加載器稀余。
類加載器(ClassLoader)用來加載 Java 類到 Java 虛擬機(jī)中。
JVM 中內(nèi)置了三個(gè)重要的 ClassLoader趋翻,同時(shí)按如下順序進(jìn)行加載:
-
BootstrapClassLoader 啟動(dòng)類加載器:最頂層的加載類睛琳,由C++實(shí)現(xiàn),負(fù)責(zé)加載
%JAVA_HOME%/lib
目錄下的核心jar包和類或者或被-Xbootclasspath
參數(shù)指定的路徑中的所有類踏烙。 -
ExtensionClassLoader 擴(kuò)展類加載器:主要負(fù)責(zé)加載目錄
%JRE_HOME%/lib/ext
目錄下的jar包和類师骗,或被java.ext.dirs
系統(tǒng)變量所指定的路徑下的jar包。 - AppClassLoader 應(yīng)用程序類加載器:面向我們用戶的加載器讨惩,負(fù)責(zé)加載當(dāng)前應(yīng)用classpath下的所有jar包和類辟癌。
除了 BootstrapClassLoader 其他類加載器均由 Java 實(shí)現(xiàn)且全部繼承自java.lang.ClassLoader
:
類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時(shí)步脓,我們還可以自定義類加載器。
需要注意的是,Java虛擬機(jī)對(duì)Class文件采用的是按需加載的方式靴患,也就是說當(dāng)需要使用該類時(shí)才會(huì)將它的Class文件加載到內(nèi)存生成Class對(duì)象仍侥。
雙親委派模型#
概念#
每一個(gè)類都有一個(gè)對(duì)應(yīng)它的類加載器。在加載類的時(shí)候鸳君,是采用的雙親委派模型农渊,即把請(qǐng)優(yōu)求先交給父類處理的一種任務(wù)委派模式。
系統(tǒng)中的類加載器在協(xié)同工作的時(shí)候會(huì)默認(rèn)使用 雙親委派模型 或颊。
雙親委派模型的理論很簡(jiǎn)單砸紊,分為如下幾步:
即在類加載的時(shí)候,系統(tǒng)會(huì)首先判斷當(dāng)前類是否被加載過囱挑。已經(jīng)被加載的類會(huì)直接返回醉顽,否則才會(huì)嘗試加載。
加載的時(shí)候平挑,首先會(huì)把該請(qǐng)求委派給該父類加載器的
loadClass()
處理游添,因此所有的請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器BootstrapClassLoader
中。當(dāng)父類加載器無法處理時(shí)通熄,才由自己來處理唆涝。
AppClassLoader的父類加載器為ExtensionClassLoader ,ExtensionClassLoader 的父類加載器為null唇辨,當(dāng)父類加載器為null時(shí)廊酣,會(huì)使用啟動(dòng)類加載器 BootstrapClassLoader 作為父類加載器。
為什么要使用雙親委派模型#
試想一種情況赏枚,我們?cè)陧?xiàng)目目錄下亡驰,手動(dòng)創(chuàng)建了一個(gè)java.lang
包,并在該包下創(chuàng)建了一個(gè)Object
嗡贺,這時(shí)候我們?cè)偃?dòng)Java程序隐解,原生Object
會(huì)被篡改嗎?當(dāng)然是不會(huì)的诫睬!
因?yàn)?code>Object類是Java的核心庫類煞茫,由BootstrapClassLoader加載,而自定義的java.lang.Object
類應(yīng)該是由AppClassLoader來加載摄凡。
BootstrapClassLoader先于AppClassLoader進(jìn)行加載续徽,根據(jù)上面的雙親委派模型的概念,我們可以知道亲澡,java.lang.Object
類已經(jīng)被加載钦扭,并且AppClassLoader要加載類之前都要先給其父類過目,所以自己寫的野類是無法撼動(dòng)核心庫類的床绪。
結(jié)論
雙親委派模型保證了Java程序的穩(wěn)定運(yùn)行客情,可以避免類的重復(fù)加載其弊,也保證了 Java 的核心 API 不被篡改。
源碼分析#
雙親委派模型的都集中在 java.lang.ClassLoader
的 loadClass()
中膀斋,相關(guān)代碼如下所示:
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先梭伐,檢查請(qǐng)求的類是否已經(jīng)被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//父加載器不為空,調(diào)用父加載器loadClass()方法處理
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//父加載器為空仰担,使用啟動(dòng)類加載器 BootstrapClassLoader 加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//拋出異常說明父類加載器無法完成加載請(qǐng)求
}
if (c == null) {
long t1 = System.nanoTime();
//自己嘗試加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
反雙親委派模型#
雙親委派模型是Java默認(rèn)的糊识,假如我們不想用雙親委派,我們要怎么辦呢摔蓝?
我們可以自定義一個(gè)類加載器赂苗,除了 BootstrapClassLoader
其他類加載器均由 Java 實(shí)現(xiàn)且全部繼承自java.lang.ClassLoader
。如果我們要自定義自己的類加載器贮尉,很明顯需要繼承 ClassLoader
拌滋。
從上面的源碼我們知道,雙親委派模型的都集中在 java.lang.ClassLoader
的 loadClass()
中绘盟,如果想打破雙親委派模型則需要重寫 loadClass()
方法鸠真。
如果我們不想打破雙親委派模型,就重寫
ClassLoader
類中的findClass()
方法即可龄毡,無法被父類加載器加載的類最終會(huì)通過這個(gè)方法被加載卖擅。