面試官:說說類加載的幾個(gè)階段
一、摘要
我們知道 Java 是先通過編譯器將.java類文件轉(zhuǎn)成.class字節(jié)碼文件,然后再通過虛擬機(jī)將.class字節(jié)碼文件加載到內(nèi)存中來實(shí)現(xiàn)應(yīng)用程序的運(yùn)行。
那么虛擬機(jī)是什么時(shí)候加載class文件?如何加載class文件?class文件進(jìn)入到虛擬機(jī)后發(fā)生了哪些變化?
今天我們就一起來了解一下,虛擬機(jī)是如何加載類文件的。
二、類加載的時(shí)機(jī)
經(jīng)常有面試官問,“類什么時(shí)候加載”和“類什么時(shí)候初始化”,從內(nèi)容上來說,似乎都在問同一個(gè)問題:class文件是什么時(shí)候被虛擬機(jī)加載到內(nèi)存中,并進(jìn)入可以使用的狀態(tài)?
從虛擬機(jī)角度來說,加載和初始化是類的加載過程中的兩個(gè)階段。
對于“什么時(shí)候加載”,Java 虛擬機(jī)規(guī)范中并沒有約束,每個(gè)虛擬機(jī)實(shí)例都可以按自身需要來自由實(shí)現(xiàn)。但基本上都遵循類在進(jìn)行初始化之前,需要先進(jìn)行加載class文件。
對于“什么時(shí)候初始化”,Java 虛擬機(jī)規(guī)范有明確的規(guī)定,當(dāng)符合以下條件時(shí)(包括但不限),并且虛擬機(jī)在內(nèi)存中沒有找到對應(yīng)的類信息,必須對類進(jìn)行“初始化”操作:
- 使用new實(shí)例化對象時(shí),讀取或者設(shè)置一個(gè)類的靜態(tài)字段或方法時(shí)
- 反射調(diào)用時(shí),例如Class.forName("com.xxx.Test")
- 初始化一個(gè)類的子類,會(huì)首先初始化子類的父類
- Java 虛擬機(jī)啟動(dòng)時(shí)標(biāo)明的啟動(dòng)類,比如main方法所在的類
- JDK8 之后,接口中存在default方法,這個(gè)接口的實(shí)現(xiàn)類初始化時(shí),接口會(huì)在它之前進(jìn)行初始化
類在初始化開始之前,需要先經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析這四個(gè)階段的操作。
下面我們一起來看看類的加載過程。
三、類的加載過程
當(dāng)一個(gè)類需要被加載到虛擬機(jī)中執(zhí)行時(shí),虛擬機(jī)會(huì)通過類加載器,將其.class文件中的字節(jié)碼信息在內(nèi)存中轉(zhuǎn)化成一個(gè)具體的java.lang.Class對象,以便被調(diào)用執(zhí)行。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存,整個(gè)生命周期包括七個(gè)階段:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載,可以用如下圖來簡要概括。
圖片
其中類加載的過程,可以用三個(gè)步驟(五個(gè)階段)來簡要描述:加載 -> 連接(驗(yàn)證、準(zhǔn)備、解析)-> 初始化。(驗(yàn)證、準(zhǔn)備、解析這3個(gè)階段統(tǒng)稱為連接)
其次加載、驗(yàn)證、準(zhǔn)備和初始化這四個(gè)階段發(fā)生的順序是確定的,必須按照這種順序按部就班的開始,而解析階段則不一定。在某些情況下解析階段可以在初始化階段之后開始,這是為了支持 Java 語言的運(yùn)行時(shí)綁定,也稱為動(dòng)態(tài)綁定或晚期綁定。
同時(shí),這五個(gè)階段并不是嚴(yán)格意義上的按順序完成,在類加載的過程中,這些階段會(huì)互相混合,可能有些階段完成了,有些階段沒有完成,會(huì)交叉運(yùn)行,最終完成類的加載和初始化。
接下來依此分解一下加載、驗(yàn)證、準(zhǔn)備、解析、初始化這五個(gè)步驟,這五個(gè)步驟組成了一個(gè)完整的類加載過程。使用沒什么好說的,卸載通常屬于 GC 的工作,當(dāng)一個(gè)類沒有被任何地方引用并且類加載器已被 GC 回收,GC 會(huì)將當(dāng)前類進(jìn)行卸載,在后續(xù)的文章我們會(huì)介紹 GC 的工作機(jī)制。
3.1、加載
加載是類加載的過程的第一個(gè)階段,這個(gè)階段的主要工作是查找并加載類的二進(jìn)制數(shù)據(jù),在虛擬機(jī)中,類的加載有兩種觸發(fā)方式:
- 預(yù)先加載:指的是虛擬機(jī)啟動(dòng)時(shí)加載,例如JAVA_HOME/lib/下的rt.jar下的.class文件,這個(gè)jar包里面包含了程序運(yùn)行時(shí)常用的文件內(nèi)容,例如java.lang.*、java.util.*、java.io.*等等,因此會(huì)隨著虛擬機(jī)啟動(dòng)時(shí)一起加載到內(nèi)存中。要證明這一點(diǎn)很簡單,自己可以寫一個(gè)空的main函數(shù),設(shè)置虛擬機(jī)參數(shù)為-XX:+TraceClassLoading,運(yùn)行程序就可以獲取類加載的全部信息
- 運(yùn)行時(shí)加載:虛擬機(jī)在用到一個(gè).class文件的時(shí)候,會(huì)先去內(nèi)存中查看一下這個(gè).class文件有沒有被加載,如果沒有,就會(huì)按照類的全限定名來加載這個(gè)類;如果有,就不會(huì)加載。
無論是哪種觸發(fā)方式,虛擬機(jī)在加載.class文件時(shí),都會(huì)做以下三件事情:
- 1.通過類的全限定名定位.class文件,并獲取其二進(jìn)制字節(jié)流
- 2.將類信息、靜態(tài)變量、字節(jié)碼、常量這些.class文件中的內(nèi)容放入運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)中
- 3.在內(nèi)存中生成一個(gè)代表這個(gè).class文件的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口,一般這個(gè)java.lang.Class對象會(huì)存在 Java 堆中
虛擬機(jī)規(guī)范對這三點(diǎn)的要求并不具體,因此具體虛擬機(jī)實(shí)現(xiàn)的靈活度都很大。比如第一條,沒有指明二進(jìn)制字節(jié)流要從哪里來,單單就這一條,就能變出許多花樣來,比如下面幾種加載方式:
- 從 zip、jar、ear、war 等歸檔文件中加載.class文件
- 通過網(wǎng)絡(luò)下載并加載.class文件,典型應(yīng)用就是 Applet
- 將Java源文件動(dòng)態(tài)編譯為.class文件,典型應(yīng)用就是動(dòng)態(tài)代理技術(shù)
- 從數(shù)據(jù)庫中提取.class文件并進(jìn)行加載
總的來說,加載階段(準(zhǔn)確地說,是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)對于開發(fā)者來說是可控性最強(qiáng)的一個(gè)階段。因?yàn)殚_發(fā)者既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義類加載器來完成加載。
3.2、驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
Java 語言本身是比較安全的語言,但是正如上面說到的.class文件未必是從 Java 源碼編譯而來,可以使用任何途徑來生成并加載。虛擬機(jī)如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對自身保護(hù)的一項(xiàng)重要工作。
驗(yàn)證階段大致會(huì)完成 4 項(xiàng)檢驗(yàn)工作:
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,例如:是否以0xCAFEBABE開頭、主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型等
- 元數(shù)據(jù)驗(yàn)證:對字節(jié)碼描述的元數(shù)據(jù)信息進(jìn)行語義分析,要符合 Java 語言規(guī)范,例如:是否繼承了不允許被繼承的類(例如 final 修飾過的)、類中的字段、方法是否和父類產(chǎn)生矛盾等等
- 字節(jié)碼驗(yàn)證:對類的方法體進(jìn)行校驗(yàn)分析,確保這些方法在運(yùn)行時(shí)是合法的、符合邏輯的
- 符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行,例如:確保符號(hào)引用的全限定名能找到對應(yīng)的類,符號(hào)引用中的類、字段、方法允許被當(dāng)前類所訪問等等
驗(yàn)證階段是非常重要的,但不是必須的,它對程序運(yùn)行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。
3.3、準(zhǔn)備
準(zhǔn)備是連接階段的第二步,這個(gè)階段的主要工作是正式為類變量分配內(nèi)存并設(shè)置其初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。
不過這個(gè)階段,有幾個(gè)知識(shí)點(diǎn)需要注意一下:
- 1.這時(shí)候進(jìn)行內(nèi)存分配的僅僅是類變量(被static修飾的變量),而不是實(shí)例變量,實(shí)例變量將會(huì)在對象實(shí)例化的時(shí)候隨著對象一起分配在 Java 堆中
- 2.這個(gè)階段會(huì)設(shè)置變量的初始值,值為數(shù)據(jù)類型默認(rèn)的零值(如 0、0L、null、false 等),不是在代碼中被顯式地賦予的值;但是當(dāng)字段被final修飾時(shí),這個(gè)初始值就是代碼中顯式地賦予的值
- 3.在 JDK1.8 取消永久代后,方法區(qū)變成了一個(gè)邏輯上的區(qū)域,這些類變量的內(nèi)存實(shí)際上是分配在 Java 堆中的,跟 JDK1.7 及以前的版本稍有不同
關(guān)于第二個(gè)知識(shí)點(diǎn),我們舉個(gè)簡單的例子進(jìn)行講解,比如public static int value = 123,value在準(zhǔn)備階段過后是0而不是123。
因?yàn)檫@時(shí)候尚未開始執(zhí)行任何 Java 方法,把value賦值為123的public static指令是在程序編譯后存放于類構(gòu)造器<clinit>()方法之中的,因此把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
假如被final修飾,比如public static final int value = 123就不一樣了,編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段,虛擬機(jī)就會(huì)給value賦值為123,因?yàn)檫@個(gè)變量無法被修改,會(huì)存入類的常量池中。
各個(gè)數(shù)據(jù)類型的零值如下圖:
數(shù)據(jù)類型 | 零值 |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
boolean | false |
char | \u0000 |
reference | null |
3.4、解析
解析是連接階段的第三步,這個(gè)階段的主要工作是虛擬機(jī)會(huì)把這個(gè).class文件中常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用。
主要解析的是類或接口、字段、方法等符號(hào)引用,我們可以把解析階段中符號(hào)引用轉(zhuǎn)換為直接引用的過程,理解為當(dāng)前加載的這個(gè)類和它所引用的類,正式進(jìn)行“連接“的過程。
我們先來了解一下符號(hào)引用和直接引用有什么區(qū)別:
- 符號(hào)引用:這個(gè)其實(shí)是屬于編譯原理方面的概念,Java 代碼在編譯期間,是不知道最終引用的類型,具體指向內(nèi)存中哪個(gè)位置的,這時(shí)候會(huì)使用一個(gè)符號(hào)引用來表示具體引用的目標(biāo)是"誰",符號(hào)引用和虛擬機(jī)的內(nèi)存布局是沒有關(guān)系的
- 直接引用:指的是可以直接或間接指向目標(biāo)內(nèi)存位置的指針或句柄,直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局是有關(guān)系的
符號(hào)引用轉(zhuǎn)換為直接引用,可以理解成將某個(gè)符號(hào)與虛擬機(jī)中的內(nèi)存位置建立連接,通過指針或句柄來直接訪問目標(biāo)。
與此同時(shí),同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)現(xiàn)上翻譯出來的直接引用一般不會(huì)相同。
3.5、初始化
初始化是類加載的過程的最后一步,這個(gè)階段的主要工作是執(zhí)行類構(gòu)造器 <clinit>()方法的過程。
簡單的說,初始化階段做的事就是給static變量賦予用戶指定的值,同時(shí)類中如果存在static代碼塊,也會(huì)執(zhí)行這個(gè)靜態(tài)代碼塊里面的代碼。
初始化階段,虛擬機(jī)大致依此會(huì)進(jìn)行如下幾個(gè)步驟的操作:
- 1.檢查這個(gè)類是否被加載和連接,如果沒有,則程序先加載并連接該類
- 2.檢查該類的直接父類有沒有被初始化,如果沒有,則先初始化其直接父類
- 3.類中如果有多個(gè)初始化語句,比如多個(gè)static代碼塊,則依次執(zhí)行這些初始化語句
有個(gè)地方需要注意的是:虛擬機(jī)會(huì)保證類的初始化在多線程環(huán)境中被正確地加鎖、同步執(zhí)行,所以無需擔(dān)心是否會(huì)出現(xiàn)變量初始化時(shí)線程不安全的問題。
如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都會(huì)阻塞等待,直到<clinit>()方法執(zhí)行完畢。同時(shí),同一個(gè)類加載器下,一個(gè)類只會(huì)初始化一次,如果檢查到當(dāng)前類沒有初始化,執(zhí)行初始化;反之,不會(huì)執(zhí)行初始化。
與此同時(shí),只有當(dāng)對類的主動(dòng)使用的時(shí)候才會(huì)觸發(fā)類的初始化,觸發(fā)時(shí)機(jī)主要有以下幾種場景:
- 1.創(chuàng)建類的實(shí)例對象,比如new一個(gè)對象操作
- 2.訪問某個(gè)類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
- 3.調(diào)用類的靜態(tài)方法
- 4.反射操作,比如Class.forName("xxx")
- 5.初始化某個(gè)類的子類,則其父類也會(huì)被初始化,并且父類具有優(yōu)先被初始化的優(yōu)勢
- 6.Java 虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類,比如SpringBootApplication入口類
最后,<clinit>()方法和<init>()方法是不同的,一個(gè)是類構(gòu)造器初始化,一個(gè)是實(shí)例構(gòu)造器初始化,千萬別搞混淆了啊。
3.6、小結(jié)
當(dāng)一個(gè)符合 Java 虛擬機(jī)規(guī)范的.class字節(jié)碼文件,經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析、初始化這些 5 個(gè)階段相互協(xié)作執(zhí)行完成之后,虛擬機(jī)會(huì)將此文件的二進(jìn)制數(shù)據(jù)導(dǎo)入運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆內(nèi)存中,創(chuàng)建一個(gè)java.lang.Class類的對象,這個(gè)對象描述了這個(gè)類所有的信息,同時(shí)提供了這個(gè)類在方法區(qū)的訪問入口。
可以用如下圖來簡要描述。
圖片
與此同時(shí),在方法區(qū)中,使用同一加載器的情況下,每個(gè)類只會(huì)有一份Class字節(jié)流信息;在堆內(nèi)存中,使用同一加載器的情況下,每個(gè)類也只會(huì)有一份java.lang.Class類的對象。
四、類加載器
在上文類的加載過程中,我們有提到在加載階段,通過一個(gè)類的全限定名來獲取此類的二進(jìn)制字節(jié)流操作,其實(shí)類加載器就是用來實(shí)現(xiàn)這個(gè)操作的。
在虛擬機(jī)中,任何一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間,對于類也同樣如此。
簡單的說,在虛擬機(jī)中看兩個(gè)類是否相同,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則即使這兩個(gè)類來源于同一個(gè).class文件,被同一個(gè)虛擬機(jī)加載,但是它們的類加載器不同,這兩個(gè)類必定不相等。
當(dāng)年為了滿足瀏覽器上 Java Applet 的需求,Java 的開發(fā)團(tuán)隊(duì)設(shè)計(jì)了類加載器,它獨(dú)立于 Java 虛擬機(jī)外部,同時(shí)也允許用戶按自身需要自行實(shí)現(xiàn)類加載器。通過類加載器,可以讓同一個(gè)類可以實(shí)現(xiàn)訪問隔離、OSGi、程序熱部署等等場景。發(fā)展至今,類加載器已經(jīng)是 Java 技術(shù)體系的一塊重要基石。
4.1、類加載器介紹
如果要查找類加載器,通過Thread.currentThread().getContextClassLoader()方法可以獲取。
簡單示例如下:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:" + loader);
System.out.println("parent loader:" + loader.getParent());
System.out.println("parent parent loader:" + loader.getParent().getParent());
}
}
輸出結(jié)果如下:
current loader:sun.misc.Launcher$AppClassLoader@18b4aac2
parent loader:sun.misc.Launcher$ExtClassLoader@511d50c0
parent parent loader:null
從運(yùn)行結(jié)果可以看到,當(dāng)前的類加載器是AppClassLoader,它的上一級是ExtClassLoader,再上一級是null。
其實(shí)ExtClassLoader的上一級是有類加載器的,它叫Bootstrap ClassLoader,是一個(gè)啟動(dòng)類加載器,由 C++ 實(shí)現(xiàn),不是 ClassLoader 子類,因此以 null 作為結(jié)果返回。
這幾種類加載器的層次關(guān)系,可以用如下圖來描述。
圖片
它們之間的啟動(dòng)流程,可以通過以下內(nèi)容來簡單描述:
- 1.在虛擬機(jī)啟動(dòng)后,會(huì)優(yōu)先初始化Bootstrap Classloader
- 2.接著Bootstrap Classloader負(fù)責(zé)加載ExtClassLoader,并且將 ExtClassLoader的父加載器設(shè)置為Bootstrap Classloader
- 3Bootstrap Classloader加載完ExtClassLoader后,就會(huì)加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader
因此,在加載 Java 應(yīng)用程序中的class文件時(shí),這里的父類加載器并不是通過繼承關(guān)系來實(shí)現(xiàn)的,而是互相配合進(jìn)行加載。
站在虛擬機(jī)的角度,只存在兩種不同的類加載器:
- 啟動(dòng)類加載器:它由 C++ 實(shí)現(xiàn)(這里僅限于 Hotspot,不同的虛擬機(jī)可能實(shí)現(xiàn)不太一樣),是虛擬機(jī)自身的一部分
- 其它類加載器:這些類加載器都由 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī)之外,并且全部繼承自抽象類java.lang.ClassLoader,比如ExtClassLoader、AppClassLoader等,這些類加載器需要由啟動(dòng)類加載器加載到內(nèi)存中之后才能去加載其他的類
站在開發(fā)者的角度,類加載器大致可以劃分為三類:
- 啟動(dòng)類加載器:比如Bootstrap ClassLoader,負(fù)責(zé)加載<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)制定的路徑,例如jre/lib/rt.jar里所有的class文件。同時(shí),啟動(dòng)類加載器是無法被 Java 程序直接引用的
- 拓展類加載器:比如Extension ClassLoader,負(fù)責(zé)加載 Java 平臺(tái)中擴(kuò)展功能的一些 jar 包,包括<JAVA_HOME>\lib\ext目錄中或java.ext.dirs指定目錄下的 jar 包。同時(shí),開發(fā)者可以直接使用擴(kuò)展類加載器
- 應(yīng)用程序類加載器:比如Application ClassLoader,負(fù)責(zé)加載ClassPath路徑下所有 jar 包,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下它就是程序中默認(rèn)的類加載器
當(dāng)然,如果有必要,也可以自定義類加載器,因?yàn)?JVM 自帶的 ClassLoader 只懂得從本地文件系統(tǒng)中加載標(biāo)準(zhǔn)的class文件,如果要從特定的場所取得class文件,例如數(shù)據(jù)庫中和網(wǎng)絡(luò)中,此時(shí)可以自己編寫對應(yīng)的 ClassLoader 類加載器。
4.2、雙親委派模型
在上文中我們提到,在虛擬機(jī)中,任何一個(gè)類由加載它的類加載器和這個(gè)類一同來確立其唯一性。
也就是說,JVM 對類的唯一標(biāo)識(shí),可以簡單的理解為由ClassLoader id + PackageName + ClassName組成,因此在一個(gè)運(yùn)行程序中有可能存在兩個(gè)包名和類名完全一致的類,但是如果這兩個(gè)類不是由一個(gè) ClassLoader 加載,會(huì)被視為兩個(gè)不同的類,此時(shí)就無法將一個(gè)類的實(shí)例強(qiáng)轉(zhuǎn)為另外一個(gè)類,這就是類加載器的隔離性。
為了解決類加載器的隔離問題,JVM 引入了雙親委派模型。
雙親委派模式,可以用一句話來說表達(dá):任何一個(gè)類加載器在接到一個(gè)類的加載請求時(shí),都會(huì)先讓其父類進(jìn)行加載,只有父類無法加載(或者沒有父類)的情況下,才嘗試自己加載。
大致流程圖如下:
圖片
使用雙親委派模式,可以保證,每一個(gè)類只會(huì)有一個(gè)類加載器。例如 Java 最基礎(chǔ)的 Object 類,它存放在 rt.jar 之中,這是 Bootstrap 的職責(zé)范圍,當(dāng)向上委派到 Bootstrap 時(shí)就會(huì)被加載。
但如果沒有使用雙親委派模式,可以任由自定義加載器進(jìn)行加載的話,Java 這些核心類的 API 就會(huì)被隨意篡改,無法做到一致性加載效果。
JDK 中ClassLoader.loadClass()類加載器中的加載類的方法,部分核心源碼如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1.首先要保證線程安全
synchronized (getClassLoadingLock(name)) {
// 2.先判斷這個(gè)類是否被加載過,如果加載過,直接跳過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 3.有父類,優(yōu)先交給父類嘗試加載;如果為空,使用BootstrapClassLoader類加載器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父類加載失敗,這里捕獲異常,但不需要做任何處理
}
// 4.沒有父類,或者父類無法加載,嘗試自己加載
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;
}
}
4.3、自定義類加載器
在上文中我們提及過,針對某些特定場景,比如通過網(wǎng)絡(luò)來傳輸 Java 類的字節(jié)碼文件,為保證安全性,這些字節(jié)碼經(jīng)過了加密處理,這時(shí)系統(tǒng)提供的類加載器就無法對其進(jìn)行加載,此時(shí)我們可以自定義一個(gè)類加載器來完成文件的加載。
自定義類加載器也需要繼承ClassLoader類,簡單示例如下:
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}
return defineClass(name, data, 0, data.length);
}
return null;
}
protected byte[] loadClassData(String name) {
try {
// package -> file folder
name = name.replace(".", "http://");
FileInputStream fis = new FileInputStream(new File(classPath + "http://" + name + ".class"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = -1;
byte[] b = new byte[2048];
while ((len = fis.read(b)) != -1) {
baos.write(b, 0, len);
}
fis.close();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
相關(guān)的測試類如下:
package com.example;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader:" + loader);
}
}
將ClassLoaderTest.java源文件放在指定目錄下,并通過javac命令編譯成ClassLoaderTest.class,最后進(jìn)行測試。
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
String classPath = "/Downloads";
CustomClassLoader customClassLoader = new CustomClassLoader(classPath);
Class<?> testClass = customClassLoader.loadClass("com.example.ClassLoaderTest");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
}
輸出結(jié)果:
com.example.CustomClassLoader@60e53b93
在實(shí)際使用過程中,最好不要重寫loadClass方法,避免破壞雙親委派模型。
4.4、加載類的幾種方式
在類加載器中,有三種方式可以實(shí)現(xiàn)類的加載。
- 1.通過命令行啟動(dòng)應(yīng)用時(shí)由 JVM 初始化加載,在上文已提及過
- 2.通過Class.forName()方法動(dòng)態(tài)加載
- 3.通過ClassLoader.loadClass()方法動(dòng)態(tài)加載
其中Class.forName()和ClassLoader.loadClass()加載方法,稍有區(qū)別:
- Class.forName():表示將類的.class文件加載到 JVM 中之后,還會(huì)對類進(jìn)行解釋,執(zhí)行類中的static方法塊;
- Class.forName(name, initialize, loader):支持通過參數(shù)來控制是否執(zhí)行類中的static方法塊;
- ClassLoader.loadClass():它只將類的.class文件加載到 JVM,但是不執(zhí)行類中的static方法塊,只有在newInstance()才會(huì)去執(zhí)行static方法塊;
我們可以看一個(gè)簡單的例子!
public class ClassTest {
static {
System.out.println("初始化靜態(tài)代碼塊!");
}
}
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
// 獲取當(dāng)前系統(tǒng)類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 1.使用Class.forName()來加載類,默認(rèn)會(huì)執(zhí)行初始化靜態(tài)代碼塊
Class.forName(ClassTest.class.getName());
// 2.使用Class.forName()來加載類,指定false,不會(huì)執(zhí)行初始化靜態(tài)代碼塊
// Class.forName(ClassTest.class.getName(), false, classLoader);
// 3.使用ClassLoader.loadClass()來加載類,不會(huì)執(zhí)行初始化靜態(tài)代碼塊
// classLoader.loadClass(ClassTest.class.getName());
}
}
運(yùn)行結(jié)果如下:
初始化靜態(tài)代碼塊!
切換不同的加載方式,會(huì)有不同的輸出結(jié)果!
4.5、小結(jié)
從以上的介紹中,針對類加載器的機(jī)制,我們可以總結(jié)出以下幾點(diǎn):
- 全盤負(fù)責(zé):當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class文件時(shí),該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來加載
- 雙親委派:在接受類加載請求時(shí),會(huì)讓父類加載器試圖加載該類,只有在父類加載器無法加載該類或者沒有父類時(shí),才嘗試從自己的類路徑中加載該類
- 按需加載:用戶創(chuàng)建的類,通常加載是按需進(jìn)行的,只有使用了才會(huì)被類加載器加載
- 緩存機(jī)制:有被加載過的Class文件都會(huì)被緩存,當(dāng)要使用某個(gè)Class時(shí),會(huì)先去緩存查找,如果緩存中沒有才會(huì)讀取Class文件進(jìn)行加載。這就是為什么修改了Class文件后,必須重啟 JVM,程序的修改才會(huì)生效的原因
五、小結(jié)
本文從類的加載過程到類加載器,做了一次知識(shí)內(nèi)容講解,內(nèi)容比較多,如果有描述不對的地方,歡迎大家留言指出,不勝感激!
六、參考
1.https://zhuanlan.zhihu.com/p/25228545
2.http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html
3.https://www.cnblogs.com/xrq730/p/4844915.html
4.https://www.cnblogs.com/xrq730/p/4845144.html