# 前言
在 JVM 綜述里面,我們說,JVM 做了三件事情,Java 程序的內存管理,
Java Class 二進制字節流的加載(ClassLoader),Java 程序的執行(執行引擎)。我們也說,我們大部分情況下只關注前2個。在前面的文章中,我們已經分析了內存關系相關的,包括運行時數據區,GC 相關。今天我們要講的就是類加載器。
在 JVM 綜述 里,我們已經大致分析了一些概念。而今天的文章將詳細的闡述類加載器。
首先,我們要了解類加載器,當然,了解的目的是為了更好的開發,通過對類加載器的解讀,看看我們能不能做些什么,比如修改類加載器的加載邏輯,比如加入自定義的類加載器等等功能。
讓我們開始吧!
# 1. 類加載器介紹
對于 Java 虛擬機來說,Class 文件是一個重要的接口,無論使用何種語言進行軟件開發,只要能將源文件編譯為正確的 Class 文件,那么這種語言就可以在 Java 虛擬機上運行。可以說,Class 文件就是虛擬機的基石。
如圖所示:
從上圖可以看出,虛擬機不拘泥于 Java 語言,任何一個源文件只要能編譯成 Class 文件的格式,就可以在JVM 上運行!Class 文件格式就像是一個接口,只要遵守這個接口,就能夠在 JVM 上運行。
# 2. 類加載器的工作流程
Class 文件通常是以文件的方式存在(任何二進制流都可以是 Class 類型),但只有能被 JVM 加載后才能被使用,才能運行編譯后的代碼。系統裝在 Class 類型可以分為加載,鏈接和初始化三個步驟。其中,鏈接也可分為驗證,準備和解析3步驟。如圖所示:
其中,只有加載過程是程序員能夠控制的,后面的幾個步驟都是有虛擬機自動運行的。因此,我們的關注點主要放在加載階段。
# 3. 類加載流程中的 “加載”
上面說了,類加載器3個流程中,唯一能讓程序員 “做手腳” 的就是加載過程,上面是加載過程呢?其主要作用就是從系統外部獲得 Class 二進制數據流。
JVM 不會無故裝載 Class 文件,只有在必要的時候才裝載,哪幾個時候呢?
- 當創建一個類的實例是,比如使用 new 關鍵字,或者通過反射,克隆,反序列化。
- 當調用類的靜態方法時,即當使用字節碼 invokstatic 指令。
- 當使用類或接口的靜態字段時(final 常量除外),比如,使用 getstatic 或者 pustatic 指令。
- 當時用 Java.lang.reflect 包中的方法反射類的方法時。
- 當初始化子類,要求先初始化父類。
- 作為啟動虛擬機,含有 main()方法的那個類。
以上6種情況屬于主動調用,主動調用會觸發初始化,還有一種情況是被動調用,則不會引起初始化。
# 3.1 ClassLoader 抽象類介紹
Java 類加載器的具體實現就在 java.lang.ClassLoader,該類是一個抽象類,并且提供了一些重要的接口,用于自定義Class 的加載流程和加載方式。主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
給定一個類名,加載一個雷,返回代表這個類的 Class 實例,如果找不到類,則返回異常。protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
根據給定的字節碼流 b 定義一個類,off 表示位置,len 表示長度。該方法只有子類可以使用。protected Class<?> findClass(String name) throws ClassNotFoundException
查找一個類,也是只能子類使用,這是重載 ClassLoader 時,最重要的系統擴展點。這個方法會被 loadClass 調用,用于自定義查找類的邏輯,如果不需要修改類加載默認機制,只是想改變類加載的形式,就可以重載該方法。protected final Class<?> findLoadedClass(String name)
同樣的,這個方法也只有子類能夠使用,他會去尋找已經加載的類,這個方法是 final 方法,無法被修改。
同時,在該類中,還有一個字段非常重要:parent,他也是一個 ClassLoader 的實例,這個字段所表示的 ClassLoader 也稱為這個 ClassLoader 的雙親,在類加載的過程中,ClassLoader 可能會將某些請求交給自己的雙親處理。
# 3.2 類加載器的雙親委派模型
在標準的 Java 程序中,從虛擬機的角度講,只有2種類加載器:
- 啟動類加載器(BootStrap ClassLoader),C++ 語言實現,虛擬機自身的一部分
- 另一種就是所有其他的類加載器,由 Java 語言實現,獨立于虛擬機外部,并且全部繼承自抽身類 java.lang.ClassLoader。
從程序員的角度講,虛擬機會創建 3 中類加載器,分別是:Bootstrap ClassLoader(啟動類加載器),Extension ClassLoader(擴展類加載器)和 APPClassLoader(應用類加載器,也稱為系統類加載器)。此外,每一個應用程序還可以擁有自定義的 ClassLoader,擴展 Java 虛擬機獲取 Class 數據的能力。
而這 3 個類加載器有著層次關系。
先來看一個著名的圖:
如圖所示:從 ClassLoader 的層次自頂向下為啟動類加載器,擴展類加載器,應用類加載器和自定義類加載器,當系統需要適用一個類時,在判斷類是否已經被加載時,會先從當前底層類加載器進行判斷,但系統需要加載一個類時,會從頂層類開始加載,依次向下嘗試,直到成功。
注意,我們無法訪問啟動類加載器,當試圖獲取啟動類加載器的時候,返回 null,因此,如果返回的是 null,并不意味沒有類加載器為它服務,而是指哪個類為啟動類加載器。
那么這些類加載路徑是哪些呢?
BootStrap 類加載器負責加載 <JAVA_HOME>/lib 目錄中的,或者別-Xbootclasspath 參數指定的路徑。并且是被虛擬機識別的,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會加載。
擴展類加載器有 sun.misc.Launcher$ExtClassLoader 實現,負責加載 <JAVA_HOME>/lib/ext 目錄中的。或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫。
應用類加載器由 sun.misc.Launcher$AppClassLoader 實現,由于這個類是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也稱為系統類加載器,負載加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器。一般情況下,這個就是程序中默認的類加載器。
自定義類加載器用于加載一些特殊途徑的類,一般也是用戶程序類。
系統中的 ClassLoader 在協同工作時,默認會使用雙親委托模式,即在類加載的時候,系統會判斷當前類是否已經被加載,如果已經加載,則直接返回,否則就嘗試加載,在嘗試加載時,會先請求雙親處理,如果雙親查找事變,則自己加載。代碼如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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;
}
}
代碼中,如果雙親是 null,則使用啟動類加載器加載,如果事變,則使用當前類加載器加載。
雙親為 null 一般有2種情況,1. 雙親是啟動類加載器。2. 自己就是啟動類加載器。
其中加載類的邏輯有2個注意的地方。
判斷是否已經加載?當判斷類是否需要加載時,是從底層開始判斷,如果底層已經加載了,則不再請求雙親。
當系統準備加載一個類時。會先從雙親加載,也就是最頂層的啟動類加載器,逐層向下,直到找到該類。和上面的是相反的。
# 3.3 類加載器的雙親委派模型缺陷和補充
雙親模型固然有著優點,能夠讓整個系統保持了類的唯一性。但在有些場合,卻不適合,也就是說,頂層的啟動類加載器的代碼無法訪問到底層的類加載器。如 rt.jar 無法中代碼無法訪問到應用類加載器。
你肯定要問,為什么需要訪問呢?
在 Java 平臺中,把核心類(rt.jar)中提供外部服務,可由應用層自行實現的接口,通常可以稱為 Service Provider Interface,即 SPI。
在 rt.jar 中的抽象類需要加載繼承他們的在應用層的子類實現,但是以目前的雙親機制是無法實現的。
因此 JDK 引用了一個不太優雅的設計,上下文類加載器。也就是講類加載放在線程上下文變量中。通過 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 這兩個方法獲取和設置 ClassLoader,這樣,rt.jar 中的代碼就可以獲取到底層的類加載了。
# 3.4 突破雙親模式
雙親模式是虛擬機的默認行為,但并非必須這么做,通過重載 ClassLoader 可以修改該行為。事實上,很多框架和軟件都修改了,比如 Tomcat,OSGI。具體實現則是通過重寫 loadClass 方法,改變類的加載次序。比如先使用自定義類加載器加載,如果加載不到,則交給雙親加載。
# 4. 類加載的擴展---熱替換
我們知道:由不同的 ClassLoader 加載的同名類屬于不同的類型,不能相互轉化和兼容。
而這個特性就是我們實現熱替換的關鍵。過程如圖所示:
# 總結
好了,到這里,基本的類加載器就介紹結束了。我們總結了類加載的工作流程,包括加載,連接,初始化。然后我們重點介紹了加載,因為加載階段是我們程序員唯一有所作為的地方。然后介紹了加載階段的一些細節,比如雙親委派,然后說了雙親委派的缺點和補充,然后探討了如何修改默認的類加載方式,最后通過類加載的特性實現了熱替換。當然也看了核心類 ClassLoader 的源碼。不過,這肯定不是類加載器的全部。我們將在后面的文章中將類加載的其他特性一一解開。
good luck!!!!