JAVA 雙親委派與類加載器
雙親委派
虛擬機在加載類的過程中需要使用類加載器進行加載,而在Java
中,類加載器有很多,那麼當JVM想要加載一個.class文件的時候,到底應該由哪個類加載器加載呢?
這就不得不提到”雙親委派機制”。
首先,我們需要知道的是,Java
語言系統中支持以下4種類加載器:
- Bootstrap ClassLoader 啓動類加載器
- Extention ClassLoader 標準擴展類加載器
- Application ClassLoader 應用類加載器
- User ClassLoader 用戶自定義類加載器
這四種類加載器之間,是存在着一種層次關係的,如下圖

一般認爲上一層加載器是下一層加載器的父加載器,那麼,除了BootstrapClassLoader
之外,所有的加載器都是有父加載器的。
那麼,所謂的雙親委派機制,指的就是:當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類,而是把這個請求委託給自己的父加載器去加載。只有父加載器無法加載這個類的時候,纔會由當前這個加載器來負責類的加載。
那麼,什麼情況下父加載器會無法加載某一個類呢?
其實,Java中提供的這四種類型的加載器,是有各自的職責的:
- Bootstrap ClassLoader ,主要負責加載Java核心類庫,
%JRE_HOME%\lib
下的rt.jar、resources.jar、charsets.jar和class等。 - Extention ClassLoader,主要負責加載目錄
%JRE_HOME%\lib\ext
目錄下的jar包和class文件。 - Application ClassLoader ,主要負責加載當前應用的
classpath
下的所有類 - User ClassLoader , 用戶自定義的類加載器,可加載指定路徑的
class
文件
爲什麼使用雙親委派
通過委派的方式,可以避免類的重複加載,當父加載器已經加載過某一個類時,子加載器就不會再重新加載這個類。
另外,通過雙親委派的方式,還保證了安全性。因爲Bootstrap ClassLoader
在加載的時候,只會加載JAVA_HOME
中的jar包裏面的類,如java.lang.Integer
,那麼這個類是不會被隨意替換的,除非有人跑到你的機器上, 破壞你的JDK。
那麼,就可以避免有人自定義一個有破壞功能的java.lang.Integer
被加載。這樣可以有效的防止核心Java API被篡改。
“父子加載器”之間的關係是繼承嗎?
很多人看到父加載器、子加載器這樣的名字,就會認爲Java
中的類加載器之間存在着繼承關係。
甚至網上很多文章也會有類似的錯誤觀點。
這裏需要明確一下,雙親委派模型中,類加載器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼的。
如下爲ClassLoader中父加載器的定義:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}
雙親委派的實現
實現雙親委派的代碼都集中在java.lang.ClassLoader
的loadClass()
方法之中:
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;
}
}
主要就是以下幾個步驟:
- 先檢查類是否已經被加載過
- 若沒有加載則調用父加載器的
loadClass()
方法進行加載 - 若父加載器爲空則默認使用啓動類加載器作爲父加載器。
- 如果父類加載失敗,拋出
ClassNotFoundException
異常後,再調用自己的findClass()
方法進行加載。
如何主動破壞雙親委派機制?
知道了雙親委派模型的實現,那麼想要破壞雙親委派機制就很簡單了。
因爲他的雙親委派過程都是在loadClass
方法中實現的,那麼想要破壞這種機制,那麼就自定義一個類加載器,重寫其中的loadClass
方法,使其不進行雙親委派即可。
loadClass()、findClass()、defineClass()區別
ClassLoader
中和類加載有關的方法有很多,前面提到了loadClass,除此之外,還有findClass
和defineClass
等,那麼這幾個方法有什麼區別呢?
- loadClass()
- 就是主要進行類加載的方法,默認的雙親委派機制就實現在這個方法中。
- findClass()
- 根據名稱或位置加載.class字節碼
- definclass()
- 把字節碼轉化爲Class
如果你想定義一個自己的類加載器,並且要遵守雙親委派模型,那麼可以繼承ClassLoader
,並且在findClass
中實現你自己的加載邏輯即可。
JDBC 加載SPI接口實現類
JDBC中DrvierManager
如典型的JDBC服務,我們通常通過以下方式創建數據庫連接:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");
在以上代碼執行之前,DriverManager
會先被類加載器加載,因爲java.sql.DriverManager
類是位於rt.jar下面的 ,所以他會被根加載器加載。
類加載時,會執行DriverManager
類的靜態方法。其中有一段關鍵的代碼是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這段代碼,會嘗試加載classpath
下面的所有實現了Driver接口的實現類。
那麼,問題就來了。
DriverManager
是被根加載器加載的,那麼在加載時遇到以上代碼,會嘗試加載所有Driver
的實現類,但是這些實現類基本都是第三方提供的,根據雙親委派原則,第三方的類不能被根加載器加載。
那麼,怎麼解決這個問題呢?
於是,就在JDBC中通過引入Thread ContextClassLoader
(線程上下文加載器,默認情況下是AppClassLoader
)的方式破壞了雙親委派原則。
Thread ContextClassLoader 線程上下文類加載器
這個ClassLoader
可以通過 java.lang.Thread
類的setContextClassLoaser()
方法進行設置;如果創建線程時沒有設置,則它會從父線程中繼承;如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認爲AppClassLoader
。
public class Thread implements Runnable {
// 這裏省略了無關代碼
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 這裏省略了無關代碼
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader; // 繼承父線程的 上下文類加載器
// 這裏省略了無關代碼
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 這裏省略了無關代碼
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
}
有了Thread ContextClassLoader
,就可以實現父ClassLoader
讓子ClassLoader
去完成一個類的加載任務,即父ClassLoader
加載的類中,可以使用ContextClassLoader
去加載其無法加載的類)。
ServiceLoader load
DriverManager
類在被加載的時候就會執行通過ServiceLoader#load
方法來加載數據庫驅動(即Driver
接口的實現)。
簡單考慮以上代碼的類加載過程爲:可以想一下,DriverManager
類由BootstrapClassLoader
加載,DriverManager
類依賴於ServiceLoader
類,因此BootstrapClassLoader
也會嘗試加載ServiceLoader
類,這是沒有問題的;
再往下,ServiceLoader
的load
方法中需要加載數據庫(MySQL等)驅動包中Driver
接口的實現類,即ServiceLoader
類依賴這些驅動包中的類,此時如果是默認情況下,則還是由BootstrapClassLoader
來加載這些類,但驅動包中的Driver
接口的實現類是位於CLASSPATH
下的,BootstrapClassLoader
是無法加載的。
在ServiceLoader#load
方法中實際是指明瞭由Thread ContextClassLoader
來加載驅動包中的類:
public final class ServiceLoader<S> implements Iterable<S> {
// 省略無關代碼
public static <S> ServiceLoader<S> load(Class<S> service) {
// 需要注意的是,這裏使用的是 當前線程的 ContextClassLoader 來加載實現,這也是 ContextClassLoader 爲什麼存在的原因。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}