雙親委派機制
需求: 在默認情況下,一個限定名的類只會被一個類加載器加載并解析使用,這樣在程序中妙蔗,他就是不唯一的,不會產生歧義疆瑰。
如何實現(xiàn)這種需求眉反?
JVM的開發(fā)者引入了雙親委派模型,這個名字聽上去很高大上穆役,其實邏輯非常簡單寸五,我們通過這張圖來理解一下:
解釋一下這張圖,也就是說:在被動的情況下孵睬,當一個類收到加載請求播歼,他不會首先自己去加載,而是傳遞給自己的父親加載器掰读,這樣所有的類都會傳遞到最上層的Bootstrap ClassLoader 秘狞,只有父親加載器無法完成加載,那么此時兒子加載器才會自己去嘗試加載蹈集,什么叫無法加載烁试?就是根據(jù)類的限定名類加載器沒有在自己負責的加載路徑中找到該類,這里注意:父親加載器拢肆、兒子加載器减响,不同于父加載器靖诗,子加載器,因為上圖中這些箭頭并不表示繼承關系支示,而是一種邏輯關系刊橘,實際上是通過組合的方式來實現(xiàn)的,這也是很多博客上沒有寫清楚的容易誤導人的一點颂鸿。接下來我們就通過源碼來看下雙親委派機制具體是怎么實現(xiàn)的促绵。
代碼很簡單(取自java.lang.ClassLoader):
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 {
// parent == null 代表 parent為bootstrap classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 說明parent加載不了,當前l(fā)oader嘗試 findclass
// 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;
}
}
首先檢查該類是否已經(jīng)被加載過嘴纺,如果沒有败晴,則開啟加載流程,如果有栽渴,則直接讀取緩存尖坤。parent變量代表了當前classloader的父親加載器,這里就體現(xiàn)了闲擦,不是通過繼承而是通過組合的方式實現(xiàn)類加載器之間的 父子關系慢味。如果parent==null,約定parent是bootstrap classloader 佛致,因為最開始我們也說過贮缕,bootstrap classloader 是由JVM內部實現(xiàn)的,沒有辦法被程序引用俺榆,所以這里就約定為null感昼,當parent為null,就調用findBootstrapClassOrNull這個方法罐脊,讓bootstrap classloader 嘗試進行加載定嗓,如果parent不為null,那么就讓parent根據(jù)限定名去嘗試加載該類萍桌,并返回class對象宵溅。如果返回的class對象為null,那么就說明parent沒有能力去加載這個類上炎,那么就調用findClass恃逻,findClass表示如何去尋找該限定名的class需要各個類加載器自己實現(xiàn),比如Extension ClassLoader 和Application ClassLoader都使用了這段邏輯來實現(xiàn)自己的findClass藕施。
(取自java.net.URLClassLoader)
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
這里可以看到寇损,通過將類的限定名轉化為文件path,再通過ucp這個對象去進行尋找裳食,找到文件資源后矛市,再調用defineClass去進行類加載的后續(xù)流程,
defineClass 方法(java.net.URLClassLoader)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();
// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
defineClass 方法是由java.lang.ClassLoader中一個被final修飾的方法诲祸,意味著獲取到class二進制流以后呢浊吏,最終將會由java.lang.classloader 來進行后續(xù)的操作而昨,因為它是被final修飾的,即不允許被外部重寫找田,這符合了我們最開始所說的類加載過程中除了讀取二進制流的操作外剩余邏輯都是有JVM內部實現(xiàn)的設計歌憨,這就是雙親委派模型。
我們在看一下上回提到的兩個問題:
問題:
1.不同的類加載器午阵,除了讀取二進制流的動作和范圍不一樣躺孝,后續(xù)的加載器邏輯是否也不一樣?
2.遇到限定名一樣的類底桂,這么多類加載器會不會產生混亂?
解答:
1.我們認為除了Bootstrap ClassLoader惧眠,所有的非Bootstrap ClassLoader都繼承了java.lang.ClassLoader籽懦,都由這個類的defineClass進行后續(xù)處理。
2.越核心的類庫越被上層的類加載器加載氛魁,而某限定名的類一旦被加載過了暮顺,被動情況下,就不會再加載相同限定名的類秀存。這樣捶码,就能夠有效避免混亂。
破壞雙親委派
第一次破壞雙親委派
但是雙親委派模型或链,并不是一個具有強約束力的模型惫恼。因為它存在設計缺陷,在大部分被動情況下澳盐,也就是上層開發(fā)者正常寫代碼祈纯,沒有騷操作的情況下,他是生效并且好用的叼耙。在一些情況下腕窥,雙親委派模型可以被主動破壞,細心的同學可能已經(jīng)發(fā)現(xiàn)了筛婉,我上面自己寫的用于被證明類加載器存在命名空間的demo就是一次對雙親委派模型的破壞簇爆,可以看到,這里自定義的類加載器直接重寫了java.lang.ClassLoader的loadClass方法爽撒,而雙親委派的邏輯就是存在于這個方法內的入蛆,那么我的這個重寫就代表了對原有雙親委派邏輯的破壞,所以就出現(xiàn)了一個限定名對應兩種不同class的情況匆浙,
需要提出的 是安寺,除非是有特殊的業(yè)務場景,一般來說不要去主動破壞雙親委派模型首尼,那么JVM推薦并希望開發(fā)者遵循雙親委派模型挑庶,那么為什么不把loadClass方法像defineClass方法一樣設定成final來修飾言秸?那這樣的情況,就沒有辦法去重寫loadClass方法迎捺,也就代表著上層開發(fā)者盡量遵循雙親委派的邏輯了举畸。
因為這是JVM開發(fā)者必須面對,但是無法解決的問題凳枝,java.lang.ClassLoader 的loadClass方法抄沮,在java很早的版本就有了,而雙親委派模型是在JDK1.2引入的特性岖瑰,Java是向下兼容的叛买,也就是說,引入雙親委派機制時蹋订,世界上已經(jīng)存在了很多像上面一樣的代碼率挣。JVM既然無法拒絕支持,只能默默接受露戒,一點補救措施呢椒功,就是在JDK1.2版本后引入了findClass方法,推薦用戶去重寫該方法而不是直接重寫loadClass方法智什,這樣就毅然能符合雙親委派动漾,這是史上第一次破壞雙親委派。
第二次破壞雙親委派
我們舉個例子:比如JDK想要提供操作數(shù)據(jù)庫的功能荠锭。
那么數(shù)據(jù)庫有很多種旱眯,并且隨著時間的推移,將會出現(xiàn)更多的品種的數(shù)據(jù)庫节沦,比較合理的方式是键思,JDK提供一組規(guī)范、一組接口甫贯,各個不同的數(shù)據(jù)庫廠商按照這個接口去自己實現(xiàn)自己類庫吼鳞。
這里就問題就出現(xiàn)了:
對JDK代碼包中的加載肯定使用了上層的類加載器,比如說bootstrapClassLoader 但當你去調用JDK 中的接口時叫搁,接口所在的類將會引起第三方類庫的加載這就不符合自下而上的委派加載順序了赔桌,而是出現(xiàn)了上層類加載器放下身段去調用下層類加載器的情況,這就產生了對雙親委派模型的破壞渴逻。
這就是Java的SPI
我們可以把SPI理解成一種服務發(fā)現(xiàn)機制疾党,各大廠商的服務注冊到JDK提供的接口上,上層在調用JDK的接口時惨奕,JDBC是SPI的其中一種功能雪位,在上面的例子中我們在JDBC上注冊了mysql Driver,h2 Driver這兩種服務梨撞,那么這里SPI究竟是如何對雙親委派進行破壞的呢雹洗,我們看一下DriverManager的源碼來簡單看一下:
可以看到DriverManager會主動的對第三方Driver進行加載香罐,掃描到所有注冊為java.sql.driver類型的第三方類就使用serviceLoader去進行加載,而serviceLoader內部使用了當前線程context中的類加載器时肿,一般線程context中的類加載器默認為application ClassLoader 庇茫,所以這些第三方類也就能夠被正常加載了,所以再結合這些輸出內容螃成。
第三次破壞雙親委派
隨著人們對模塊化的追求旦签,希望在程序運行時,能夠動態(tài)的對部分組件代碼進行替換寸宏,這就是所謂的熱替換宁炫、熱部署,想想也能夠大致猜到击吱,這里又將會出現(xiàn)很多的自由的類加載操作淋淀,所以又將是一次對雙親委派模型的踐踏。
問題: 能不能自己寫一個限定名為java.lang.String的類覆醇,并在程序中調用它?