JVM(六):探究類載入過程-下
上文說了類載入過程的5個階段,著重介紹了各個階段做的工作。在本文中,我們對執行載入階段的主體進行探討,學習類載入器的模型和邏輯,以及我們該如何自定義一個類載入器。
定義
前面說過載入階段是一個可以讓設計人員高度自控的模組,因為類檔案的源頭可以是多種多樣的,程式碼生成、反射生成或從網路中生成等。因此類載入器作為對這些檔案的處理就顯得尤為重要。
但類載入器的功能不僅如此,其還有一個重要的功能就是和一個類的全限定名唯一確定一個類。通俗來說,要說兩個類是相同的,不僅其全限定名要一樣,其對應的類載入器也必須相同,才能說明兩個類是相等的。
正因為類載入器的功能角色如此重要,因此虛擬機器對其的實現規範也十分重視。在Java虛擬機器中,對其的實現模型是雙親委派模型。
模型
雙親委派模型的主要執行過程示意圖如上所示,其分為啟動類載入器(Bootstrap Class-loader),擴充類載入器(Extension Class-loader),應用程式類載入(Application Class-loader)。
其中啟動類載入器主要負責載入 JRE 的核心類庫,如 JRE 目錄下的 rt.jar。但其實根據《深入分析 Java Web 技術內幕》上所說,啟動類載入器並不嚴格符合雙親委派模型,因為Bootstrap Class-loader 並不屬於 JVM 的類等級層次。Bootstrap Class-loader 是沒有子類的,Extension Class-loader 也是沒有父類的。不過在這裡我們並不深究,只要知道有這一點就可以了。
Extension Class-loader 主要負責載入 JRE 擴充目錄 ext 下的類。
Application Class-loader 主要負責使用者類路徑(Class-path)下的類,這個類載入器是使用的最多的,因為大大多數情況下,一般開發者並沒有實現自定義的類載入器,那麼 JVM 就會使用這個來載入類大部分類。
執行過程
上圖就是雙親委派模型的執行過程,當類開始載入的時候,先檢查是否已經被載入過,如果沒有被載入過,則呼叫父類的載入方法,如果父類載入失敗,丟擲異常,則呼叫自身的 findClass() 方法進行載入。
JDK 中載入過程的原始碼分析:
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 {
// 無父親就呼叫 bootstarp 載入器來載入
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父載入器和 bootstarp 載入器都沒有找到指定類,呼叫當前類的 findClass() 來完成類載入
// 因此,自定義類載入器,就是重寫 findClass() 方法
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;
}
}
從原始碼中,我們可以看到其實符合規範要求的 雙親委派模型 的。而當我們要自定義一個類載入器的時候就是通過重寫 findClass() 來實現的。
自定義類載入器
/**
* 1. 自定義類載入器通過整合ClassLoader來實現,主要通過重寫findClass方法
* 2. findClass方法首先通過自定義的loadByte()方法將Class檔案轉換成byte[]位元組流
* 3. 然後通過defineClass()方法將其轉換為Class物件
*/
public class SelfClassLoader extends ClassLoader {
private String classPath;
public SelfClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 通過 difineClass,將一個位元組陣列轉換為Class 物件
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 根據路徑將指定的檔案讀取為byte 流
* @param name
* @return
* @throws IOException
*/
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
另一個種實現自定義類載入器的方法:
/**
* 1. 載入指定packageName下的類
* 2. 用自定義類載入器進行載入,如果載入失敗,再交給父載入器進行載入
*/
public class UrlSelfClassloader extends URLClassLoader {
private String packageName = "";
public UrlSelfClassloader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> aClass = findLoadedClass(name);
if (Objects.nonNull(aClass)){
return aClass;
}
if (!packageName.startsWith(name)){
return super.loadClass(name);
}else {
return findClass(name);
}
}
}
如何使用自定義的類載入器
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("");
Class clazz = classLoader.loadClass("");
Object obj = clazz.newInstance();
Method helloMethod = clazz.getDeclaredMethod("hello", null);
helloMethod.invoke(obj, null);
}
總結
在本文中,我們講解了類載入器的實現模型,分析了在 JDK 中類載入器的原始碼實現,並根據原始碼中的程式碼實現,自定義了一個類載入器的實現。
此外相信經過五和六兩篇文章的學習,大家應該對如何將類載入入虛擬機器中有了系統的理解。
後面的文章中,我們就要進入 JVM 的內部了,從下篇文章開始,我們就開始逐步講解 JVM 的記憶體佈局,瞭解 JVM 中的各個邏輯上劃分的儲存結構以及其作用,歡迎各位讀者瀏覽。
文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!
本系列文章主要借鑑《深入分析 Java Web 技術內幕》和《深入理解 Java 虛擬機器 - JVM 高階特性與最佳實踐》。