雙親委派模型
類加載這個概念應該算是Java語言的一種創新,目的是為了將類的加載過程與虛擬機解耦,達到”通過類的全限定名來獲取描述此類的二進制字節流“的目的。實現這個功能的代碼模塊就是類加載器。類加載器的基本模型就是大名鼎鼎的雙親委派模型(Parents Delegation Model)。聽上去很牛掰,其實邏輯很簡單,在需要加載一個類的時候,我們首先判斷該類是否已被加載,如果沒有就判斷是否已被父加載器加載,如果還沒有再調用自己的findClass方法嘗試加載。基本的模型就是這樣(盜圖侵刪):
實現起來也很簡單,重點就是ClassLoader類的loadClass方法,源碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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) { Class(c); } return c; } } |
突然感覺被逗了,怎么默認直接就拋了異常呢?其實是因為ClassLoader這個類是一個抽象類,實際在使用時候會寫個子類,這個方法會按照需要被重寫,來完成業務需要的加載過程。
自定義ClassLoader
在自定義ClassLoader的子類時候,我們常見的會有兩種做法,一種是重寫loadClass方法,另一種是重寫findClass方法。其實這兩種方法本質上差不多,畢竟loadClass也會調用findClass,但是從邏輯上講我們最好不要直接修改loadClass的內部邏輯。
個人認為比較好的做法其實是只在findClass里重寫自定義類的加載方法。
為啥說這種比較好呢,因為前面我也說道,loadClass這個方法是實現雙親委托模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此我們最好是在雙親委托模型框架內進行小范圍的改動,不破壞原有的穩定結構。同時,也避免了自己重寫loadClass方法的過程中必須寫雙親委托的重復代碼,從代碼的復用性來看,不直接修改這個方法始終是比較好的選擇。
當然,如果是刻意要破壞雙親委托模型就另說。
破壞雙親委托模型
為什么要破壞雙親委托模型呢?
其實在某些情況下,我們可能需要加載兩個不同的類,但是不巧的是這兩個類的名字完全一樣,這時候雙親委托模型就無法滿足我們的要求了,我們就要重寫loadClass方法破壞雙親委托模型,讓同一個類名加載多次。當然,這里說的破壞只是局部意義上的破壞。
但是類名相同了,jvm怎么區別這兩個類呢?顯然,這并不會造成什么世界觀的崩塌,其實類在jvm里并不僅是通過類名來限定的,他還屬于加載他的ClassLoader。由不同ClassLoader加載的類其實是互不影響的。
做一個實驗。
我們先寫兩個類:
1
2
3
4
5
6
|
package com.mythsman.test; public class Hello { public void say() { System.out.println("This is from Hello v1"); } } |
1
2
3
4
5
6
|
package com.mythsman.test; public class Hello { public void say() { System.out.println("This is from Hello v2"); } } |
兩個類名字一樣,唯一的區別是方法的實現不一樣。我們先分別編譯,然后把生成的class文件重命名為Hello.class.1和Hello.class.2。
我們的目的是希望能在測試類里分別創建這兩個類的實例。
接著我們新建一個測試類com.mythsman.test.Main,在主函數里創建兩個自定義的ClassLoader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
ClassLoader classLoader1=new ClassLoader() { @Override public Class<?> loadClass(String s) throws ClassNotFoundException { try { if (s.equals("com.mythsman.test.Hello")) { byte[] classBytes = Files.readAllBytes(Paths.get("/home/myths/Desktop/test/Hello.class.1")); return defineClass(s, classBytes, 0, classBytes.length); }else{ return super.loadClass(s); } }catch (IOException e) { throw new ClassNotFoundException(s); } } }; ClassLoader classLoader2=new ClassLoader() { @Override public Class<?> loadClass(String s) throws ClassNotFoundException { try { if (s.equals("com.mythsman.test.Hello")) { byte[] classBytes = Files.readAllBytes(Paths.get("/home/myths/Desktop/test/Hello.class.2")); return defineClass(s, classBytes, 0, classBytes.length); }else{ return super.loadClass(s); } }catch (IOException e) { throw new ClassNotFoundException(s); } } };< br > |
這兩個ClassLoader的用途就是分別關聯Hello類的兩種不同字節碼,我們需要讀取字節碼文件并通過defineClass方法加載成class。注意我們重載的是loadClass方法,如果是重載findClass方法那么由于loadClass方法的雙親委托處理機制,第二個ClassLoader的findClass方法其實并不會被調用。
那我們怎么生成實例呢?顯然我們不能直接用類名來引用(名稱沖突),那就只能用反射了:
1
2
3
4
|
Object helloV1=classLoader1.loadClass("com.mythsman.test.Hello").newInstance(); Object helloV2=classLoader2.loadClass("com.mythsman.test.Hello").newInstance(); helloV1.getClass().getMethod("say").invoke(helloV1); helloV2.getClass().getMethod("say").invoke(helloV2); |
輸出:
1
2
|
This is from Hello v1 This is from Hello v2 |
OK,這樣就算是完成了兩次加載,但是還有幾個注意點需要關注下。
兩個類的關系是什么
顯然這兩個類并不是同一個類,但是他們的名字一樣,那么類似isinstance of之類的操作符結果是什么樣的呢:
1
2
3
4
5
6
|
System.out.println("class:"+helloV1.getClass()); System.out.println("class:"+helloV2.getClass()); System.out.println("hashCode:"+helloV1.getClass().hashCode()); System.out.println("hashCode:"+helloV2.getClass().hashCode()); System.out.println("classLoader:"+helloV1.getClass().getClassLoader()); System.out.println("classLoader:"+helloV2.getClass().getClassLoader()); |
輸出:
1
2
3
4
5
6
|
class:class com.mythsman.test.Hello class:class com.mythsman.test.Hello hashCode:1581781576 hashCode:1725154839 classLoader:com.mythsman.test.Main$1@5e2de80c classLoader:com.mythsman.test.Main$2@266474c2 |
他們的類名的確是一樣的,但是類的hashcode不一樣,也就意味著這兩個本質不是一個類,而且他們的類加載器也不同(其實就是Main的兩個內部類)。
這兩個類加載器跟系統的三層類加載器是什么關系
以第一個自定義的類加載器為例:
1
2
3
4
5
|
System.out.println(classLoader1.getParent().getParent().getParent()); System.out.println(classLoader1.getParent().getParent()); System.out.println(classLoader1.getParent()); System.out.println(classLoader1 ); System.out.println(ClassLoader.getSystemClassLoader()); |
輸出:
1
2
3
4
5
|
null sun.misc.Launcher$ExtClassLoader@60e53b93 sun.misc.Launcher$AppClassLoader@18b4aac2 com.mythsman.test.Main$1@5e2de80c sun.misc.Launcher$AppClassLoader@18b4aac2 |
當然,這里說的父子關系并不是繼承關系,而是組合關系,子ClassLoader保存了父ClassLoader的一個引用(parent)。