JVM 第三篇:Java 類加載機制

?本文內(nèi)容過于硬核,建議有 Java 相關(guān)經(jīng)驗人士閱讀。
?
1. 什么是類的加載?
類的加載指的是將類的 .class 文件中的二進制數(shù)據(jù)讀入到內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個 java.lang.Class 對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的 Class 對象, Class 對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向 Java 程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
類加載器并不需要等到某個類被 「首次主動使用」 時再加載它, JVM 規(guī)范允許類加載器在預(yù)料某個類將要被使用時就預(yù)先加載它,如果在預(yù)先加載的過程中遇到了 .class 文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤。
加載.class文件的方式
–?從本地系統(tǒng)中直接加載
–?通過網(wǎng)絡(luò)下載.class文件
–?從zip,jar等歸檔文件中加載.class文件
–?從專有數(shù)據(jù)庫中提取.class文件
–?將Java源文件動態(tài)編譯為.class文件
2. 類的生命周期
一個類型從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統(tǒng)稱為連接(Linking)。

加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定特性(也稱為動態(tài)綁定或晚期綁定)。
2.1 加載(Loading)
加載時類加載過程的第一個階段,在加載階段,虛擬機需要完成以下三件事情:
通過一個類的全限定名來獲取其定義的二進制字節(jié)流。
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
相對于類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節(jié)流的動作)是可控性最強的階段,因為開發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
加載階段完成后,虛擬機外部的 二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中,而且在 Java 堆中也創(chuàng)建一個 java.lang.Class 類的對象,這樣便可以通過該對象訪問方法區(qū)中的這些數(shù)據(jù)。
2.2 驗證(Verification)
驗證是連接階段的第一步,這一階段的目的是確保 Class 文件的字節(jié)流中包含的信息符合「Java虛擬機規(guī)范」的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全。
驗證是連接階段的第一步,這一階段的目的是為了確保 Class 文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證: 驗證字節(jié)流是否符合 Class 文件格式的規(guī)范;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規(guī)范的要求;例如:這個類是否有父類,除了 java.lang.Object 之外。 字節(jié)碼驗證: 通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。 符號引用驗證: 確保解析動作能正確執(zhí)行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用 -Xverifynone 參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
2.3 準備(Preparation)
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。對于該階段有以下幾點需要注意:
這時候進行內(nèi)存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。 這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認的零值(如 0 、 0L 、 null 、 false 等),而不是被在 Java 代碼中被顯式地賦予的值。
2.4 初始化(Initialization)
類的初始化階段是類加載過程的最后一個步驟,之前介紹的幾個類加載的動作里,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外,其余動作都完全由 Java 虛擬機來主導(dǎo)控制。直到初始化階段, Java 虛擬機才真正開始執(zhí)行類中編寫的 Java 程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。
在 Java 中對類變量進行初始值設(shè)定有兩種方式:
聲明類變量是指定初始值。 使用靜態(tài)代碼塊為類變量指定初始值。
3. 類加載器
類加載器就是負責加載所有的類,將其載入內(nèi)存中,生成一個 java.lang.Class 實例。一旦一個類被加載到 JVM 中之后,就不會再次載入了。

啟動類加載器(Bootstrap ClassLoader):其負責加載 Java 的核心類,比如 String 、 System 這些類。 拓展類加載器(Extension ClassLoader):其負責加載 JRE 的拓展類庫。 系統(tǒng)類加載器(System ClassLoader):其負責加載 CLASSPATH 環(huán)境變量所指定的 JAR 包和類路徑。 用戶類加載器:用戶自定義的加載器,以類加載器為父類。
一個簡單的小栗子:
public?static?void?main(String[]?args)?{
????ClassLoader?loader?=?ClassLoader.getSystemClassLoader();
????System.out.println(loader);
????System.out.println(loader.getParent());
????System.out.println(loader.getParent().getParent());
}
輸出結(jié)果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
「為什么根類加載器為 NULL ?」
啟動類加載器(Bootstrap Loader)并不是 Java 實現(xiàn)的,而是使用 C 語言實現(xiàn)的,找不到一個確定的返回父 Loader 的方式,于是就返回 null 。
「JVM 類加載機制」
全盤負責:當一個類加載器負責加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。 父類委托:先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。 緩存機制,緩存機制將會保證所有加載過的 Class 都會被緩存,當程序中需要使用某個 Class 時,類加載器先從緩存區(qū)尋找該 Class ,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成 Class 對象,存入緩存區(qū)。這就是為什么修改了 Class 后,必須重啟 JVM ,程序的修改才會生效。
4. 雙親委派模型
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
雙親委派機制:
當 AppClassLoader 加載一個 class 時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。 當 ExtClassLoader 加載一個 class 時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。 如果 BootStrapClassLoader 加載失?。ɡ缭?$JAVA_HOME/jre/lib 里未查找到該 class ),會使用 ExtClassLoader 來嘗試加載。 若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,如果 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException 。
以下為 ClassLoader#loadClass 的源碼, JDK 版本為 1.8.0_221 。
protected?Class>?loadClass(String?name,?boolean?resolve)
????throws?ClassNotFoundException
{
????synchronized?(getClassLoadingLock(name))?{
????????//?首先判斷該類型是否已經(jīng)被加載
????????Class>?c?=?findLoadedClass(name);
????????if?(c?==?null)?{
????????????//?如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載
????????????long?t0?=?System.nanoTime();
????????????try?{
????????????????if?(parent?!=?null)?{
????????????????????//?如果存在父類加載器,就委派給父類加載器加載
????????????????????c?=?parent.loadClass(name,?false);
????????????????}?else?{
????????????????????//?如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調(diào)用本地方法?native?Class?findBootstrapClass(String?name)
????????????????????c?=?findBootstrapClassOrNull(name);
????????????????}
????????????}?catch?(ClassNotFoundException?e)?{
????????????????//?ClassNotFoundException?thrown?if?class?not?found
????????????????//?from?the?non-null?parent?class?loader
????????????}
????????????if?(c?==?null)?{
????????????????//?If?still?not?found,?then?invoke?findClass?in?order
????????????????//?to?find?the?class.
????????????????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;
????}
}
雙親委派模型是為了防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼,保證程序穩(wěn)定的運行。
5. 自定義類加載器
在最開始,我想先介紹下自定義類加載器的適用場景:
加密:Java 代碼可以輕易的被反編譯,如果需要把代碼進行加密以防止反編譯,可以先將編譯后的代碼用某種加密算法加密,這樣加密后的類就不能再用 Java 的 ClassLoader 去加載類了,這時就需要自定義 ClassLoader 在加載類的時候先解密類,然后再加載。 從非標準的來源加載代碼:如果我們的字節(jié)碼是放在數(shù)據(jù)庫、甚至是在云端,就可以自定義類加載器,從指定的來源加載類。
一個小案例,首先我們創(chuàng)建一個需要加載的目標類:
public?class?ClassLoaderTest?{
????public?void?hello()?{
????????System.out.println("我是由?"?+?getClass().getClassLoader().getClass()?+?"?加載的");
????}
}
這個類先進行編譯,編譯后的 class 我放到了 D 盤的根目錄,然后刪除原本在項目中的 class 文件,如果不刪除的話,通過前面的雙親委派模型,我們會知道這個 class 會被 sun.misc.Launcher$AppClassLoader 進行加載。
然后我們定義一個自己的加載類:
public?class?MyClassLoader?extends?ClassLoader?{
????public?MyClassLoader(){}
????public?MyClassLoader(ClassLoader?parent){
????????super(parent);
????}
????protected?Class>?findClass(String?name)?throws?ClassNotFoundException?{
????????File?file?=?new?File("D:\\ClassLoaderTest.class");
????????try{
????????????byte[]?bytes?=?getClassBytes(file);
????????????//defineClass方法可以把二進制流字節(jié)組成的文件轉(zhuǎn)換為一個java.lang.Class
????????????Class>?c?=?this.defineClass(name,?bytes,?0,?bytes.length);
????????????return?c;
????????}
????????catch?(Exception?e)?{
????????????e.printStackTrace();
????????}
????????return?super.findClass(name);
????}
????private?byte[]?getClassBytes(File?file)?throws?Exception?{
????????//?這里要讀入.class的字節(jié),因此要使用字節(jié)流
????????FileInputStream?fis?=?new?FileInputStream(file);
????????FileChannel?fc?=?fis.getChannel();
????????ByteArrayOutputStream?baos?=?new?ByteArrayOutputStream();
????????WritableByteChannel?wbc?=?Channels.newChannel(baos);
????????ByteBuffer?by?=?ByteBuffer.allocate(1024);
????????while?(true){
????????????int?i?=?fc.read(by);
????????????if?(i?==?0?||?i?==?-1)
????????????????break;
????????????by.flip();
????????????wbc.write(by);
????????????by.clear();
????????}
????????fis.close();
????????return?baos.toByteArray();
????}
????public?static?void?main(String[]?args)?throws?ClassNotFoundException,?IllegalAccessException,?InstantiationException,?NoSuchMethodException,?InvocationTargetException?{
????????MyClassLoader?classLoader?=?new?MyClassLoader();
????????Class?clazz?=?classLoader.loadClass("com.geekdigging.lesson03.classloader.ClassLoaderTest");
????????Object?obj?=?clazz.newInstance();
????????Method?helloMethod?=?clazz.getDeclaredMethod("hello",?null);
????????helloMethod.invoke(obj,?null);
????}
}
最后打印結(jié)果:
我是由?class?com.geekdigging.lesson03.classloader.MyClassLoader?加載的
參考
https://www.cnblogs.com/ityouknow/p/5603287.html
https://www.cnblogs.com/twoheads/p/10143038.html

