??在馮·諾依曼定義的計算機模型中抢蚀,任何程序都需要加載到內(nèi)存才能與 CPU進交流牛隅。字節(jié)碼.class 文件同樣需要加到內(nèi)存中,才可以實例化類振坚。“兵馬未動斋扰,糧草先行“渡八。ClassLoader 正是準(zhǔn)備糧草的先行軍啃洋,它的使命就是提前加載cass類文件到內(nèi)存中。在類加載時屎鳍,使用的是 Parents Delegation Model宏娄,譯為雙親委派樓型,如果意譯的話逮壁,則譯作“溯源委派加載模型”更加貼切孵坚。
??Java 的類加載器是一個運行時核心基礎(chǔ)設(shè)施模塊,如下圖 所示窥淆,主要是在啟動之初進行類的 Load卖宠、Link 和 Init,即加載忧饭、鏈接扛伍、初始化。
??第一步词裤,Load 階段讀取類文件產(chǎn)生二進制流刺洒,并轉(zhuǎn)化為特定的數(shù)據(jù)結(jié)構(gòu),初步校驗 cafe babe 魔法數(shù)亚斋、常量池作媚、文件度、是否有父類等帅刊,然后創(chuàng)建對應(yīng)類的 java.lang.Class 實例纸泡。
??第二步,Link 階段包括驗證赖瞒、準(zhǔn)備女揭、解析三個步驟。驗證是更詳細的校驗栏饮,比如final 是否合規(guī)吧兔、類型是否正確、靜態(tài)變量是否合理等袍嬉,準(zhǔn)備階段是為靜態(tài)變量分配內(nèi)存境蔼,并設(shè)定默認值,解析類和方法確保類與類之間的相互引用正確性伺通,完成內(nèi)存結(jié)構(gòu)布局箍土。
??第三步,Init 階段執(zhí)行類構(gòu)造器 <clinit> 方法罐监,如果賦值運算是通過其他類的靜態(tài)方法來完成的吴藻,那么會馬上解析另外一個類,在虛擬機棧中執(zhí)行完畢后通過返回值進行賦值弓柱。
??類加載是一個將.class字節(jié)碼文件實例化成 Class 對象并進行相關(guān)初始化的過程沟堡。在這個過程中侧但,JVM 會初始化繼承樹上還沒有被初始化過的所有父類,并且會執(zhí)行這個鏈路上所有未執(zhí)行過的靜態(tài)代碼塊航罗、靜態(tài)變量賦值語句等禀横。某些類在使用時,可以按需由類加載器進行加載伤哺。
??全小寫的class是關(guān)鍵字,用來定義類,而首字母大寫的 Class,它是所有 class的類燕侠。這句話理解起來有難度者祖,是因為類已經(jīng)是現(xiàn)實世界中某種事物的抽象立莉,為什么這個象還是另外一個類Class 的對象?示例代碼如下:
public class ClassTest {
// 數(shù)組類型有一個魔法屬性:length 來獲取數(shù)組長度
private static int[] array = new int[3];
private static int length = array.length;
//任何小寫 class 定義的類,也有一個魔法屬性: class ,來獲取此類的大寫 Class 類對象
private static Class<One> one = One.class;
private static Class<Another> another =Another.class;
public static void main(String[] args) throws Exception {
// 通過newInstance 方法創(chuàng)建One 和Another 的類對象 (第1處)
One oneObject = one.newInstance();
oneObject.call();
Another anotherObject = another.newInstance();
anotherObject.speak();
// 通過one 這個大寫的 Class 對象七问,獲取私有成員屬性對象 Field (第2處)
Field privateFieldInOne = one.getDeclaredField("inner");
// 設(shè)置私有對象可以訪問和修改 (第3處)
privateFieldInOne.setAccessible(true);
privateFieldInOne.set(oneObject, "world changed.");
// 成功修改類的私有屬性 inner 變量值為 world changed.
System.out.println(oneObject.getInner());
}
}
class One {
private String inner = "time files.";
public void call() {
System.out.println("hello world.");
}
public String getInner() {
return inner;
}
}
class Another {
public void speak() {
System.out.println("easy coding.");
}
}
執(zhí)行結(jié)果如下:
hello world.
easy coding.
world changed.
- 第1處說明:Class類下的newlnstance()在JDK9中已經(jīng)置為過時蜓耻,使用getDeclaredConstruclor().newInstance()的方式。這里看重說明一下new與newInstance()的區(qū)別械巡。new 是強類型校驗刹淌,可以調(diào)用任何構(gòu)造方法,在使用new操作的時候讥耗,這個類可以沒有被加載過有勾。而Class類下的newlinstance()是弱類型,只能調(diào)用無參數(shù)構(gòu)造方法,如果沒有默認構(gòu)造方法古程,就拋出InstantiationException異常;如果此構(gòu)造方法沒有權(quán)限訪問蔼卡,則拋出IllegalAccessException 異常。Java通過類加截器把類的實現(xiàn)與類的定義進行解耦挣磨,所以是實現(xiàn)面向接口編程雇逞、依賴倒置的必然選擇。
-
第2處說明:可以使用類似的方式獲取其他聲明茁裙,如注解塘砸、方法等。如下圖所示
- 第3處說明: private 成員在類外是否可以修改? 通過 setAccessible(true)操作即可使用大寫Class類的set方法修改其值晤锥。如果沒有這一步,則拋出如下異常
Exception in thread "main" java.lang.IllegalAccessException: class com.linkmiao.iot.demo.test.d202311.ClassTest cannot access a member of class com.linkmiao.iot.demo.test.d202311.One with modifiers "private"
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
??通過以上示例掉蔬,對于Class 這個“類中之王”,我們有一定的了解? 那么回到類加載中矾瘾,類加載器是如何定位到具體的類文件并讀取的呢?
??類加載器類似于原始部落結(jié)構(gòu)女轿,存在權(quán)力等級制度。最高的一層是家族中威望最高的 Bootstrap霜威,它是在JVM 啟動時創(chuàng)建的谈喳,通常由與操作系統(tǒng)相關(guān)的本地代碼實現(xiàn)是最根基的類加載器,負責(zé)裝載最核心的Java 類戈泼,比如Object婿禽、System赏僧、String等;第二層是在JDK9版本中扭倾,稱為 Platform ClassLoader淀零,即平臺類加載器,用以加載些擴展的系統(tǒng)類膛壹,比如XML驾中、加密、壓縮相關(guān)的功能類等模聋,JDK9之前的加載器是Extension ClassLoader; 第三層是Application ClassLoader 的應(yīng)用類加載器肩民,主要是加載用戶定義的CLASSPATH路徑下的類。第二链方、三層類加載器為 Java 語言實現(xiàn)持痰,用戶也可以自定義類加載器。查看本地類加載器的方式如下:
// 正在使用的類加載器: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425
ClassLoader c = TestWhoLoad.class.getClassLoader();
System.out.println(c);
// AppClassLoader 的父加載器是 PlatformClassLoader
ClassLoader c1 = c.getParent();
System.out.println(c1);
// PlatformClassLoader 的父加載器是 Bootstrap祟蚀。它是使用C++ 來實現(xiàn)的工窍,返回null
ClassLoader c2 = c1.getParent();
System.out.println(c2);
代碼上方的注釋內(nèi)容為JDK11的執(zhí)行結(jié)果。在JDK8環(huán)境中前酿,執(zhí)行結(jié)果如下
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@6e0be858
null
??最高一層的類加載器Bootstrap 是通過C/C++ 實現(xiàn)的患雏,并不存在于JVM體系內(nèi)所以輸出為 null。類加載器具有等級制度罢维,但是并非繼承關(guān)系淹仑,以組合的方式來復(fù)用父加載器的功能,這也符合組合優(yōu)先原則言津,詳細的雙親委派模型如圖所示攻人。
??低層次的當(dāng)前類加載器,不能覆蓋更高層次類加載器已經(jīng)加載的類悬槽。如果低層次的類加載器想加載一個未知類怀吻,要非常禮貌地向上逐級詢問:“請問,這個類已經(jīng)加載了嗎?”被詢問的高層次類加載器會自問兩個問題:第一初婆,我是否已加載過此類?第二蓬坡,如果沒有,是否可以加載此類?只有當(dāng)所有高層次類加載器在兩個問題上的答案均為“否”時磅叛,才可以讓當(dāng)前類加載器加載這個未知類屑咳。如上圖所示,左側(cè)箭頭向上逐級詢問是否已加載此類弊琴,直至 Bootstrap ClassLoader兆龙,然后向下逐級嘗試是否能夠加載此類,如果都加載不了敲董,則通知發(fā)起加載請求的當(dāng)前類加載器紫皇,準(zhǔn)予加載慰安。在右側(cè)的三個小標(biāo)簽里,列舉了此層類加載器主要加載的代表性類庫聪铺,事實上不止于此化焕。通過如下代碼可以查看 Bootstrap 所有已經(jīng)加載的類庫:
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (java.net.URL url : urLs) {
System.out.println(url.toExternalForm());
}
執(zhí)行結(jié)果如下:
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/classes
Bootstrap 加載的路徑可以追加,不建議修改或刪除原有加載路徑铃剔。在JVM 中加如下啟動參數(shù)撒桨,則能通過 Class.forName 正常讀取到指定類,說明此參數(shù)可以增加Bootstrap的類加載路徑:-
-Xbootclasspath/a:/users/yangguanbao/book/egsyCoding/byJdk11/src
在學(xué)習(xí)了類加載器的實現(xiàn)機制后键兜,知道雙親委派模型并非強制模型凤类,用戶可以自定義類加載器,在什么情況下需要自定義類加載器呢?
(1)隔離加載類蝶押。在某些框架內(nèi)進行中間件與應(yīng)用的模塊隔離踱蠢,把類加載到不同的環(huán)境。比如棋电,阿里內(nèi)某容器框架通過自定義類加載器確保應(yīng)用中依賴的jar包會影響到中間件運行時使用的jar 包。
(2)修改類加載方式苇侵。類的加載模型并非強制赶盔,除 Bootstrap 外,其他的加載非一定要引入榆浓,或者根據(jù)實際情況在某個時間點進行按需進行動態(tài)加載于未。
(3)擴展加載源。比如從數(shù)據(jù)庫陡鹃、網(wǎng)絡(luò)烘浦,甚至是電視機機頂盒進行加載。
(4)防止源碼泄露萍鲸。Java 代碼容易被編譯和篡改闷叉,可以進行編譯加密。那么類加載器也需要自定義脊阴,還原加密的字節(jié)碼握侧。
實現(xiàn)自定義類加載器的步驟,繼承 ClassLoader嘿期,重寫 findClass0方法品擎,調(diào)用defineClass0方法。一個簡單的類加載器實現(xiàn)的示例代碼如下:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 從自定義路徑中加載指定類
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
執(zhí)行的結(jié)果:
classloader.CustomClassLoader@5e481248
??由于中間件一般都有自己的依賴jar 包备徐,在同一個工程內(nèi)引用多個框架時萄传,往往被迫進行類的加載。按某種規(guī)則jar 包的版本被統(tǒng)一指定蜜猾,導(dǎo)致某些類存在包路徑類名相同的情況秀菱,就會引起類沖突西设,導(dǎo)致應(yīng)用程序出現(xiàn)異常。主流的容器類框架都會自定義類加載器答朋,實現(xiàn)不同中間件之間的類隔離贷揽,有效避免了類沖突。