雙親委派機制
1)什麼是雙親委派
虛擬機器在載入類的過程中需要使用類載入器進行載入,而在Java中,類載入器有很多,那麼當JVM想要載入一個.class檔案的時候,到底應該由哪個類載入器載入呢?這就不得不提到"雙親委派機制"。
首先,我們需要知道的是,Java語言系統中支援以下4種類載入器:
- 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檔案。
也就是說,一個使用者自定義的類,如com.hollis.ClassHollis 是無論如何也不會被Bootstrap和Extention載入器載入的
這四種類載入器之間,是存在著一種層次關係的,如下圖
一般認為上一層載入器是下一層載入器的父載入器,那麼,除了BootstrapClassLoader之外,所有的載入器都是有父載入器的。
那麼,所謂的雙親委派機制,指的就是:當一個類載入器收到了類載入的請求的時候,他不會直接去載入指定的類,而是把這個請求委託給自己的父載入器去載入。只有父載入器無法載入這個類的時候,才會由當前這個載入器來負責類的載入。
2)為什麼需要雙親委派
因為類載入器之間有嚴格的層次關係,那麼也就使得Java類也隨之具備了層次關係。比如一個定義在java.lang包下的類,因為它被存放在rt.jar之中,所以在被載入過程彙總,會被一直委託到Bootstrap ClassLoader,最終由Bootstrap ClassLoader所載入。
而一個使用者自定義的com.hollis.ClassHollis類,他也會被一直委託到Bootstrap ClassLoader,但是因為Bootstrap ClassLoader不負責載入該類,那麼會在由Extention ClassLoader嘗試載入,而Extention ClassLoader也不負責這個類的載入,最終才會被Application ClassLoader載入。
這種機制有幾個好處。
首先,透過委派的方式,可以避免類的重複載入,當父載入器已經載入過某一個類時,子載入器就不會再重新載入這個類。
另外,透過雙親委派的方式,還保證了安全性。假如我們自己編寫一個類java.util.Object
,它的實現可能有一定的危險性或者隱藏的bug。而我們知道Java自帶的核心類裡面也有java.util.Object
,如果JVM啟動的時候先行載入的是我們自己編寫的java.util.Object
,那麼就有可能出現安全問題!
3)載入器之間的關係
雙親委派模型中,類載入器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父載入器的程式碼的。
如下為ClassLoader中父載入器的定義:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}
4)雙親委派的實現原理
雙親委派模型對於保證Java程式的穩定運作很重要,但它的實現並不複雜。
實現雙親委派的程式碼都集中在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;
}
}
程式碼不難理解,主要就是以下幾個步驟:
1、先檢查類是否已經被載入過
2、若沒有載入則呼叫父載入器的loadClass()方法進行載入
3、若父載入器為空則預設使用啟動類載入器作為父載入器。
4、如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。
5)為什麼需要破壞雙親委派
假設我們有一個介面 PaymentService
和兩個實現類:
PaymentServiceImplA
(來自ThirdPartyA
)PaymentServiceImplB
(來自ThirdPartyB
)
如果這兩個實現類都被放在應用的類路徑下,並且你使用了雙親委派機制,載入過程如下:
- 載入請求:當你在程式碼中使用
PaymentServiceImplA
或PaymentServiceImplB
時,類載入器會收到載入請求。 - 委派機制:
- 首先,Bootstrap ClassLoader 會嘗試載入請求的類。如果該類不在 Java 核心庫中(比如
java.lang
、java.util
等),則 Bootstrap ClassLoader 會失敗。 - 然後,Extension ClassLoader 會嘗試載入。如果類仍然不在擴充套件庫中,則載入失敗。
- 接下來,Application ClassLoader(也稱為 Web ClassLoader,在 Web 應用中)會嘗試載入這個類。
- 首先,Bootstrap ClassLoader 會嘗試載入請求的類。如果該類不在 Java 核心庫中(比如
- 載入的結果:
- 如果
PaymentServiceImplA
和PaymentServiceImplB
都存在於類路徑下,Application ClassLoader 將會載入它們。但因為它們屬於不同的第三方庫,它們的類名必須是唯一的。
- 如果
可能出現的問題
- 命名衝突:如果
ThirdPartyA
和ThirdPartyB
的實現類都定義為PaymentServiceImpl
,這將導致類命名衝突,最終只有一個實現會被載入,而另一個可能會被忽略或引發錯誤。 - 版本衝突:如果
PaymentServiceImplA
和PaymentServiceImplB
依賴於不同版本的同一庫(例如,commons-logging
),它們將會載入到同一個 Application ClassLoader 中,這也可能導致執行時錯誤。
6)怎麼破壞雙親委派
知道了雙親委派模型的實現,那麼想要破壞雙親委派機制就很簡單了。
因為他的雙親委派過程都是在loadClass方法中實現的,那麼想要破壞這種機制,那麼就自定義一個類載入器,重寫其中的loadClass方法,使其不進行雙親委派即可。
loadClass()、findClass()、defineClass()區別
ClassLoader中和類載入有關的方法有很多,前面提到了loadClass,除此之外,還有findClass和defineClass等,那麼這幾個方法有什麼區別呢?
- loadClass()
- 就是主要進行類載入的方法,預設的雙親委派機制就實現在這個方法中。
- findClass()
- 根據名稱或位置載入.class位元組碼
- definclass()
- 把位元組碼轉化為Class
這裡面需要展開講一下loadClass和findClass,我們前面說過,當我們想要自定義一個類載入器的時候,並且像破壞雙親委派原則時,我們會重寫loadClass方法。
那麼,如果我們想定義一個類載入器,但是不想破壞雙親委派模型的時候呢?
這時候,就可以繼承ClassLoader,並且重寫findClass方法。findClass()方法是JDK1.2之後的ClassLoader新新增的一個方法。
/**
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
這個方法只丟擲了一個異常,沒有預設實現。
JDK1.2之後已不再提倡使用者直接覆蓋loadClass()方法,而是建議把自己的類載入邏輯實現到findClass()方法中。
因為在loadClass()方法的邏輯裡,如果父類載入器載入失敗,則會呼叫自己的findClass()方法來完成載入。
所以,如果你想定義一個自己的類載入器,並且要遵守雙親委派模型,那麼可以繼承ClassLoader,並且在findClass中實現你自己的載入邏輯即可。
7)破壞雙親委派的例子
1. Tomcat 的類載入機制
背景
Tomcat 是一個廣泛使用的 Java Web 伺服器和 Servlet 容器。它使用自己的類載入機制來處理 Web 應用和其他元件的類載入。
破壞雙親委派的原因
- 隔離性:Tomcat 需要將不同的 Web 應用(WAR 檔案)隔離開來,以防止它們之間的類衝突。不同應用可能會依賴於相同名稱的類,但這些類的實現可能不同。
- 靈活性:允許不同的 Web 應用使用不同版本的同一個庫(例如,
commons-logging
),而不影響其他應用。
實現
Tomcat 使用多個類載入器:
- Web 應用類載入器:負責載入應用的類。
- 父載入器(通常是
Application ClassLoader
):負責載入 Tomcat 自身的類和一些共享庫。
具體而言,當 Tomcat 載入一個 Web 應用時,它的類載入器會優先載入應用內的類。如果該類在應用內找不到,才會委託給父載入器。這種方式允許 Web 應用使用自己的類而不是全域性共享的類。
2. JBoss/WildFly 的類載入機制
背景
JBoss/WildFly 是另一個流行的 Java EE 應用伺服器,採用了類似的策略來處理類載入。
破壞雙親委派的原因
- 模組化:JBoss/WildFly 允許開發者將應用劃分為多個模組,每個模組可以有自己獨立的依賴。
- 防止版本衝突:允許不同模組之間使用不同版本的相同庫。
實現
- JBoss/WildFly 使用模組載入器,模組中的類預設不經過雙親委派機制。每個模組都可以有自己的類路徑,減少類衝突的風險。
3. 例子總結
例如,如果在 Tomcat 中有兩個 Web 應用:
- 應用 A 使用
commons-logging
的 1.1 版本。 - 應用 B 使用
commons-logging
的 1.2 版本。
如果不破壞雙親委派機制,兩個應用會共享同一個 commons-logging
類,這可能導致執行時錯誤和版本不相容。但由於 Tomcat 的類載入器會優先載入應用自身的類,因此各自的 commons-logging
版本會被正確載入。
8)類載入器的使用場景
什麼時候使用預設載入器
- 常規應用開發:
- 大多數 Java 應用程式、Web 應用和企業應用都可以使用預設的類載入器(
Application ClassLoader
),因為它能夠從類路徑中自動載入需要的類。
- 大多數 Java 應用程式、Web 應用和企業應用都可以使用預設的類載入器(
- 使用標準庫:
- 當你的應用僅依賴於 Java 標準庫和已經在類路徑下的第三方庫時,預設類載入器通常足夠。
什麼時候使用自定義類載入器
- 特殊路徑載入:
- 當需要從非標準路徑(如網路、資料庫或特定資料夾)載入類時,使用自定義類載入器可以滿足這種需求。
- 版本衝突管理:
- 在同一專案中需要使用多個版本的同名類時,自定義類載入器可以隔離它們,避免命名衝突。
- 動態生成類:
- 如果你的應用需要在執行時動態生成和載入類(例如,透過位元組碼操作庫),則自定義類載入器是必要的。
- 類增強和修改:
- 對於需要在類載入時進行位元組碼修改或增強(如 AOP 框架),使用自定義類載入器是合適的。
參考:https://www.cnblogs.com/hollischuang/p/14260801.html