圖解類載入器和雙親委派機制,一看就懂
聽說微信搜尋《Java魚仔》會變更強哦!
本文收錄於JavaStarter,裡面有我完整的Java系列文章,學習或面試都可以看看哦
(一)概述
我們都知道Java程式碼會被編譯成class檔案,在class檔案中描述了該類的各種資訊,class類最終需要被載入到虛擬機器中才能執行和使用。
虛擬機器把Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成虛擬機器可以直接使用的Java型別,這就是虛擬機器的類載入機制。
(二)類載入的過程
一個類從被載入到卸載出記憶體,一共包含下面七個階段:
載入、驗證、準備、解析、初始化、使用、解除安裝載入的來源有以下部分:
1、本地磁碟
2、網路下載的.class檔案
3、war,jar下載入.class檔案
4、從專門的資料庫中讀取.class檔案(少見)
5、將java原始檔動態編譯成class檔案,典型的就是動態代理,通過執行時生成class檔案
載入的過程是通過類載入器實現的。有關類載入的其他過程我會在下一章中介紹。
(三)類載入器的分類
類載入器分為系統級別和使用者級別:
系統級別的類載入器有:
1、啟動類載入器(底層使用C++實現)
2、擴充套件類載入器(底層使用java實現,是ClassLoader的子類)
3、應用程式類載入器(底層使用java實現,是ClassLoader的子類)
使用者級別的類載入器我們統一稱為自定義類載入器。
3.1 啟動類載入器
首先我們來看看啟動類載入器載入了哪些類,啟動類載入器負責載入sun.boot.class.path:
public static void bootClassLoaderLoadingPath(){
//獲取啟動列載入器載入的目錄
String bootStrapLoadingPath=System.getProperty("sun.boot.class.path");
//把載入的目錄轉為集合
List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
for (String bootPath:bootLoadingPathList){
System.out.println("啟動類載入器載入的目錄:"+bootPath);
}
}
通過上面的程式碼我們可以獲取到啟動類載入器所載入的類:
3.2 拓展類載入器
擴充套件類載入器載入負責載入java.ext.dirs,我們同樣寫一段程式碼去載入它:
public static void extClassLoaderLoadingPath(){
//獲取啟動列載入器載入的目錄
String bootStrapLoadingPath=System.getProperty("java.ext.dirs");
//把載入的目錄轉為集合
List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
for (String bootPath:bootLoadingPathList){
System.out.println("拓展類載入器載入的目錄:"+bootPath);
}
}
可以看到,除了載入了JDK目錄下的ext外,還載入了Sun目錄下的ext
3.3 應用程式類載入器
最後是應用類載入器,它負責載入java.class.path:
public static void appClassLoaderLoadingPath(){
//獲取啟動列載入器載入的目錄
String bootStrapLoadingPath=System.getProperty("java.class.path");
//把載入的目錄轉為集合
List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
for (String bootPath:bootLoadingPathList){
System.out.println("應用程式類載入器載入的目錄:"+bootPath);
}
}
它負責載入工程目錄下classpath下的class以及jar包。
(四)雙親委派模型
所謂雙親委派模型,就是指一個類接收到類載入請求後,會把這個請求依次傳遞給父類載入器(如果還有的話),如果頂層的父類載入器可以載入,就成功返回,如果無法載入,再依次給子載入器去載入。我們先通過程式碼來看一下類載入器的層級結構:
public class ClassLoaderPath {
public static void main(String[] args) {
System.out.println(ClassLoaderPath.class.getClassLoader());
System.out.println(ClassLoaderPath.class.getClassLoader().getParent());
System.out.println(ClassLoaderPath.class.getClassLoader().getParent().getParent());
}
}
編寫一個類,依次輸出這個類的類載入器,父類載入器,父類的父類載入器
可以看到首先是應用程式類載入器,它的父類是擴充套件類載入器,擴充套件類載入器的父類輸出了一個null,這個null會去呼叫啟動類載入器。如果你不信,我們看原始碼:ClassLoader類
接著從父類載入器往下呼叫findClass,如果可以載入,就直接返回class,如果不能載入,就依次向下。如果到了自定義載入器還是無法被載入,就會丟擲ClassNotFound異常。
我畫了一個流程圖來展示雙親委派模型的全過程:
雙親委派模型保證了Java程式的穩定執行,可以避免類的重複載入,也保證了 Java 的核心 API 不被篡改。
(五)破壞雙親委派
雙親委派模型並不是絕對的,spi機制就可以打破雙親委派模型。
首先我們需要了解什麼是spi,spi(Service Provider Interface)是一種服務發現機制,Java在核心庫中定義了許多介面,並且針對這些介面給出呼叫邏輯,但是並未給出具體的實現。開發者要做的就是定製一個實現類,在 META-INF/services 中註冊實現類資訊,以供核心類庫使用。最典型的就是JDBC。
Java提供了一個Driver介面用於驅動各個廠商的資料庫連線,Driver類位於JAVA_HOME中jre/lib/rt.jar中,應該由Bootstrap類載入器進行載入。根據類載入機制,當被載入的類引用了另外一個類的時候,虛擬機器就會使用載入該類的類載入器載入被引用的類,因此如果其他資料庫廠商定製了Driver的實現類之後,按理說也得把這個實現類放到啟動類載入器載入的目錄下,這顯然是很不合理的。
於是Java提供了spi機制,即使Driver由啟動類載入器去載入,但是他可以讓執行緒上下文載入器(Thread Context ClassLoader)去請求子類載入器去完成載入,預設是應用程式類載入器。但是這確實破壞了類載入機制。