最近一段時間,我在面試的過程當中,很喜歡問雙親委派的一些問題,由於我發現這個問題真的能夠幫助我全方位的瞭解一個候選人。
記得前幾天一次面試過程當中,我和一位候選人聊到了JVM的類加載機制的問題,他談到了雙親委派,而且很自信的給我講了一下他對於雙親委派的理解。
由於可貴碰到一個對着塊知識瞭解的比較多的候選人,因而咱們展開了"300回合"的交鋒,當問完這些問題的以後,大概半個小時已通過去了。
最後,這個後續人和我說:"我萬萬沒想到,我一個工做7年的技術經理,居然被雙親委派給虐了!!!"
先來回顧下我都問了他哪些問題,看看你能回答上來多少個:
三、"父加載器"和"子加載器"之間的關係是繼承的嗎?
六、爲何重寫loadClass方法能夠破壞雙親委派,這個方法和findClass()、defineClass()區別是什麼?
以上,10個問題,從頭開始答,你大概能夠堅持到第幾題?
首先,咱們知道,虛擬機在加載類的過程當中須要使用類加載器進行加載,而在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文件
那麼也就是說,一個用戶自定義的類,如com.hollis.ClassHollis 是不管如何也不會被Bootstrap和Extention加載器加載的。
如上面咱們提到的,由於類加載器之間有嚴格的層次關係,那麼也就使得Java類也隨之具有了層次關係。
好比一個定義在java.lang包下的類,由於它被存放在rt.jar之中,因此在被加載過程彙總,會被一直委託到Bootstrap ClassLoader,最終由Bootstrap ClassLoader所加載。
而一個用戶自定義的com.hollis.ClassHollis類,他也會被一直委託到Bootstrap ClassLoader,可是由於Bootstrap ClassLoader不負責加載該類,那麼會在由Extention ClassLoader嘗試加載,而Extention ClassLoader也不負責這個類的加載,最終纔會被Application ClassLoader加載。
首先,經過委派的方式,能夠避免類的重複加載,當父加載器已經加載過某一個類時,子加載器就不會再從新加載這個類。
另外,經過雙親委派的方式,還保證了安全性。由於Bootstrap ClassLoader在加載的時候,只會加載JAVA_HOME中的jar包裏面的類,如java.lang.Integer,那麼這個類是不會被隨意替換的,除非有人跑到你的機器上, 破壞你的JDK。
那麼,就能夠避免有人自定義一個有破壞功能的java.lang.Integer被加載。這樣能夠有效的防止核心Java API被篡改。
不少人看到父加載器、子加載器這樣的名字,就會認爲Java中的類加載器之間存在着繼承關係。
這裏須要明確一下,雙親委派模型中,類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼的。
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
}java
雙親委派模型對於保證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;
}
}mysql
代碼不難理解,主要就是如下幾個步驟:web
二、若沒有加載則調用父加載器的loadClass()方法進行加載
三、若父加載器爲空則默認使用啓動類加載器做爲父加載器。
四、若是父類加載失敗,拋出ClassNotFoundException異常後,再調用本身的findClass()方法進行加載。
知道了雙親委派模型的實現,那麼想要破壞雙親委派機制就很簡單了。
由於他的雙親委派過程都是在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);
}面試
這個方法只拋出了一個異常,沒有默認實現。
sql
JDK1.2以後已再也不提倡用戶直接覆蓋loadClass()方法,而是建議把本身的類加載邏輯實現到findClass()方法中。
由於在loadClass()方法的邏輯裏,若是父類加載器加載失敗,則會調用本身的findClass()方法來完成加載。
因此,若是你想定義一個本身的類加載器,而且要遵照雙親委派模型,那麼能夠繼承ClassLoader,而且在findClass中實現你本身的加載邏輯便可。
雙親委派機制的破壞不是什麼稀奇的事情,不少框架、容器等都會破壞這種機制來實現某些功能。
因爲雙親委派模型是在JDK1.2以後才被引入的,而在這以前已經有用戶自定義類加載器在用了。因此,這些是沒有遵照雙親委派原則的。
第二種,是JNDI、JDBC等須要加載SPI接口實現類的狀況。
第三種是爲了實現熱插拔熱部署工具。爲了讓代碼動態生效而無需重啓,實現方式時把模塊連同類加載器一塊兒換掉就實現了代碼的熱替換。
第五種時OSGI、Jigsaw等模塊化技術的應用。
咱們平常開發中,大多數時候會經過API的方式調用Java提供的那些基礎類,這些基礎類時被Bootstrap加載的。
可是,調用方式除了API以外,還有一種SPI的方式。
如典型的JDBC服務,咱們一般經過如下方式建立數據庫鏈接:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");
在以上代碼執行以前,DriverManager會先被類加載器加載,由於java.sql.DriverManager類是位於rt.jar下面的 ,因此他會被根加載器加載。
數據庫
類加載時,會執行該類的靜態方法。其中有一段關鍵的代碼是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這段代碼,會嘗試加載classpath下面的全部實現了Driver接口的實現類。
編程
DriverManager是被根加載器加載的,那麼在加載時遇到以上代碼,會嘗試加載全部Driver的實現類,可是這些實現類基本都是第三方提供的,根據雙親委派原則,第三方的類不能被根加載器加載。
因而,就在JDBC中經過引入ThreadContextClassLoader(線程上下文加載器,默認狀況下是AppClassLoader)的方式破壞了雙親委派原則。
咱們深刻到ServiceLoader.load方法就能夠看到:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
第一行,獲取當前線程的線程上下⽂類加載器 AppClassLoader,⽤於加載 classpath 中的具體實現類。
tomcat
咱們知道,Tomcat是web容器,那麼一個web容器可能須要部署多個應用程序。
不一樣的應用程序可能會依賴同一個第三方類庫的不一樣版本,可是不一樣版本的類庫中某一個類的全路徑名多是同樣的。
如多個應用都要依賴hollis.jar,可是A應用須要依賴1.0.0版本,可是B應用須要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。
若是採用默認的雙親委派類加載機制,那麼是沒法加載多個相同的類。
因此,Tomcat破壞雙親委派原則,提供隔離的機制,爲每一個web容器單獨提供一個WebAppClassLoader加載器。
Tomcat的類加載機制:爲了實現隔離性,優先加載 Web 應用本身定義的類,因此沒有遵守雙親委派的約定,每個應用本身的類加載器——WebAppClassLoader負責加載自己的目錄下的class文件,加載不到時再交給CommonClassLoader加載,這和雙親委派恰好相反。
近幾年模塊化技術已經很成熟了,在JDK 9中已經應用了模塊化的技術。
其實早在JDK 9以前,OSGI這種框架已是模塊化的了,而OSGI之因此可以實現模塊熱插拔和模塊內部可見性的精準控制都歸結於其特殊的類加載機制,加載器之間的關係再也不是雙親委派模型的樹狀結構,而是發展成複雜的網狀結構。
在JDK9以前,JVM的基礎類之前都是在rt.jar這個包裏,這個包也是JRE運行的基石。
這不只是違反了單一職責原則,一樣程序在編譯的時候會將不少無用的類也一併打包,形成臃腫。
在JDK9中,整個JDK都基於模塊化進行構建,之前的rt.jar, tool.jar被拆分紅數十個模塊,編譯的時候只編譯實際用到的模塊,同時各個類加載器各司其職,只加載本身負責的模塊。
Class<?> c = findLoadedClass(cn);
if (c == null) {
// 找到當前類屬於哪一個模塊
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
//獲取當前模塊的類加載器
BuiltinClassLoader loader = loadedModule.loader();
//進行類加載
c = findClassInModuleOrNull(loadedModule, cn);
} else {
// 找不到模塊信息纔會進行雙親委派
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
}
}安全
以上,從什麼是雙親委派,到如何實現與破壞雙親委派,又從破壞雙親委派的示例等多個方面全面介紹了關於雙親委派的知識。
相信經過學習本文,你必定對雙親委派機制有了更加深入的瞭解。
閱讀過本文以後,反手在簡歷上寫下:熟悉Java的類加載機制,不服來問!
![](http://static.javashuo.com/static/loading.gif)