前言
我們經常會在面試中遇到有關類加載器的問題,而作爲一名Java開發人員應該瞭解類加載器如何工作?雙親委派模型是什麼?如何打破雙親委派?爲什麼打破?等等。所以今天的主題就是聊一聊類加載器。
ClassLoader 介紹
《深入理解Java虛擬機》這本書大家都不陌生,想必我們大多數人瞭解JVM知識都是通過這本書,在該書中也詳細介紹了Java類加載的全過程,包含加載、驗證、準備、解析和初始化這5個階段。
在加載階段,通過一個類的全限定名來獲取此類的二進制字節流,就是依靠類加載器來完成。
類加載器的一個作用就是將編譯器編譯生成的二進制 Class 文件加載到內存中,進而轉換成虛擬機中的類。Java系統提供了三種內置的類加載器:
- 啓動類加載器 (Bootstrap Class Loader): 負責加載JDK核心類,通常是 rt.jar 和位於 $JAVA_HOME/jre/lib 下的核心庫.
- 擴展類加載器 (Extensions Class Loader): 負責加載\jre\lib\ext目錄下 JAR 包
- 系統類加載器 (System Class Loader):負責加載所有應用程序級別的類到JVM,它會加載classpath環境變量或 -classpath以及-cp命令行參數中指定的文件
當然,上面是 Java 默認的類加載器,我們還可以自定義類加載器,後文會分析如何自定義類加載器。
雙親委派模型是什麼
網上有文章分析說,類加載器遵循三個原則:委託性、可見性和唯一性原則。這三點其實都和雙親委派模型有關,雙親委派的工作過程如下:
當類加載器收到類的加載請求時,首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,所有的加載請求會傳送到頂層的啓動類加載器,只有父類加載器無法完成加載請求,纔會交由子加載器去加載。
三個原則的具體體現是:
-
「委託性原則」 體現在當子類加載器收到類的加載請求時,會將加載請求向上委託給父類加載器。
-
「可見性原則」 體現在允許子類加載器查看父類加載器加載的所有類,但是父類加載器不能查看子類加載器加載的類。
-
「唯一性原則」 體現在雙親委派整個機制保證了Java類的唯一性,假如你寫了一個和JRE核心類同名的類,比如Object類,雙親委派機制可以避免自定義類覆蓋核心類的行爲,因爲它首先會將加載類的請求,委託給ExtClassLoader去加載,ExtClassLoader再委託給BootstrapClassLoader,啓動類加載器如果發現已經加載了 Object類,那麼就不會加載自定義的Object類。
ClassLoader 如何工作
聊完雙親委派模型,你肯定想知道它是如何實現,那麼來看一下 ClassLoader 的核心方法,其中的 loadClass 方法就是實現雙親委派機制的關鍵,爲了縮短代碼篇幅和方便閱讀,去掉了一些代碼細節:
package java.lang;
public abstract class ClassLoader {
protected Class defineClass(byte[] b);
protected Class<?> findClass(String name);
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 檢查類是否已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2. 委託給父類加載
c = parent.loadClass(name, false);
} else {
//3. 父類不存在的,交給啓動類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
if (c == null) {
//4. 父類加載器無法完成類加載請求時,調用自身的findClass方法來完成類加載
c = findClass(name);
}
}
return c;
}
}
- defineClass 方法:調用 native 方法將 字節數組解析成一個 Class 對象。
- findClass 方法:抽象類ClassLoader中默認拋出ClassNotFoundException,需要繼承類自己去實現,目的是通過文件系統或者網絡查找類
- loadClass 方法: 首先根據類的全限定名檢查該類是否已經被加載過,如果沒有被加載,那麼當子加載器持有父加載器的引用時,那麼委託給父加載器去嘗試加載,如果父類加載器無法完成加載,再交給子類加載器進行加載。loadClass方法 就是實現了雙親委派機制。
現在我們熟悉了 ClassLoader 的三個重要方法,那麼如果需要自定義一個類加載器的話,直接繼承 ClassLoader類,一般情況只需要重寫 findClass 方法即可,自己定義加載類的路徑,可以從文件系統或者網絡環境。
但是,如果想打破雙親委派機制,那麼還要重寫 loadClass 方法,只不過,爲什麼我們要選擇去打破它呢? 我們常使用的 Tomcat的類加載器就打破了雙親委派機制,當然還有一些其他場景也打破了,比如涉及 SPI 的加載動作、熱部署等等。
接下來來看看 Tomcat 爲什麼打破雙親委派模型以及實現機制。
Tomcat如何打破雙親委派機制
爲什麼打破
現在都流行使用 springboot 開發 web 應用,Tomcat 內嵌在 springboot 中。而在此之前,我們會使用最原生的方式,servlet + Tomcat 的方式開發和部署 web 程序。web 應用的目錄結構大致如下:
| - MyWebApp
| - WEB-INF/web.xml -- 配置文件,用來配置Servlet等
| - WEB-INF/lib/ -- 存放Web應用所需各種JAR包
| - WEB-INF/classes/ -- 存放你的應用類,比如Servlet類
| - META-INF/ -- 目錄存放工程的一些信息
一個 Tomcat 可能會部署多個這樣的 web 應用,不同的 web 應用可能會依賴同一個第三方庫的不同版本,爲了保證每個 web 應用的類庫都是獨立的,需要實現類隔離。而Tomcat 的自定義類加載器 WebAppClassLoader 解決了這個問題,每一個 web 應用都會對應一個 WebAppClassLoader 實例,不同的類加載器實例加載的類是不同的,Web應用之間通各自的類加載器相互隔離。
當然 Tomcat自定義類加載器不只解決上面的問題,WebAppClassLoader 打破了雙親委派機制,即它首先自己嘗試去加載某個類,如果找不到再代理給父類加載器,其目的是優先加載Web應用定義的類。
如何打破
WebappClassLoader 具體實現機制是重寫了 ClassLoader 的 findClass 和 loadClass 方法。
- findClass 方法如下,省去部分細節:
public Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1. 先在 Web 應用目錄下查找類
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目錄沒有找到,交給父加載器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
}
//3. 如果父類也沒找到,拋出 ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
- loadClass方法如下,省去部分細節:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地緩存查找該類是否已經加載過
clazz = findLoadedClass0(name);
if (clazz != null) {
return clazz;
}
//2. 從系統類加載器的緩存中查找是否加載過
clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
//3. 嘗試用 ExtClassLoader 類加載器類加載
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 嘗試在本地目錄搜索 class 並加載
try {
clazz = findClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 嘗試用系統類加載器 (也就是 AppClassLoader) 來加載
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// 省略
}
}
//6. 上述過程都加載失敗,拋出 ClassNotFoundException 異常
throw new ClassNotFoundException(name);
}
從上面的代碼中可以看到,Tomcat 自定義的類加載器確實打破了雙親委派機制,同時根據 loadClass 方法的核心邏輯,我也畫了一張圖,描述了默認情況下 Tomcat 的類加載機制。
一開始將類加載請求委託給 ExtClassLoader,而不是委託給 AppClassLoader,這樣的原因是 防止 web 應用自己的類覆蓋JRE的核心類,如果 JRE 核心類中沒有該類,那麼才交給自定義的類加載器 WebappClassLoader 去加載。
小結
這篇文章主要總結了類加載器的雙親委派模型、雙親委派的工作機制、以及Tomcat如何打破雙親委派,當然有一些東西分享的比較簡單,比如 Tomcat 的類加載器這部分,沒有提及整個 Tomcat的類加載器層次結構,沒有提到 SharedClassLoader 和 CommonClassLoader 類加載器,這個等後續有時間再來分享。
同時,歡迎關注我新開的公衆號,定期分享Java後端知識!