類加載基礎概念
嘗試用5W1H模型來聊聊Java的類加載熄赡。
什么是類加載? 簡單的說齿税,把字節(jié)碼加載到JVM中的過程彼硫,我們就稱之為類加載。輸入是某個類的.class文件的字節(jié)流凌箕,輸出是JVM所管理的方法區(qū)中關于該類的信息拧篮。
為什么要有類加載? 我的理解是為了更好的支持動態(tài)特性牵舱,比如說熱部署串绩,就是利用了JVM可以動態(tài)加載字節(jié)碼的機制實現(xiàn)的。
什么時候進行類加載芜壁? 總的來說礁凡,JVM需要某個類的信息,而又沒有的時候慧妄,就會觸發(fā)類加載顷牌。具體來說分了以下幾個場景:
- 遇到new、getstatic塞淹、putstatic韧掩、invokestatic等指令時,如果類還沒有加載過就會觸發(fā)類加載窖铡;
- 子類進行類加載時疗锐,如果父類還沒有加載過,會先觸發(fā)父類的加載费彼;
- 使用反射進行各種操作時滑臊,如果類還沒有加載過,會先進行類加載箍铲。
- 虛擬機啟動時雇卷,會首先加載含有main方法的類。
- 其他情況,這里不是抄書关划,所以我們先不再枚舉小染。
誰來負責類加載? 類加載有專門的類加載器來完成贮折,類加載器又有等級森嚴的層級關系裤翩,爺爺輩的類加載器叫啟動類加載器,然后是爸爸輩调榄,叫拓展類加載器踊赠,最后是應用程序類加載器。這里涉及到一個類加載過程中各個類加載器是如何分工合作的每庆,會在雙親委派模型中提到筐带。
怎樣進行類加載? 前面提到過類加載就是把類的字節(jié)碼塞進虛擬機的過程缤灵,那么具體怎么做呢伦籍?
首先,類加載器需要從某處獲得字節(jié)碼的二進制字節(jié)流腮出。為什么不說字節(jié)碼文件鸽斟?因為除了從.class文件中獲取,還可以從壓縮包中解壓獲取利诺,從網(wǎng)絡中獲雀恍睢(比如Applet),甚至是動態(tài)生成一個(想想動態(tài)代理)都是可以的慢逾。這個動作立倍,我們稱為加載。(TO-DO 這個時候生成Class對象了嗎侣滩?)
接著口注,這個對象還不能直接使用,我們需要把針對它做各種校驗君珠,比如字節(jié)碼本身是否合規(guī)寝志,是否是該版本的虛擬機支持,如果都通過了策添,就需要給靜態(tài)變量開辟一塊內(nèi)存區(qū)域材部,然后賦零值,這里的零值指的是唯竹,當內(nèi)存中沒有數(shù)據(jù)時乐导,變量的值,比如對于int型來說零值是0浸颓,對于boolean型來說物臂,零值是false旺拉。最后,如果這個類中存在符號引用棵磷,還需要把符號引用解析為具體的內(nèi)存地址蛾狗。以上所有的動作,我們合并起來仪媒,稱之為鏈接沉桌。
最后,終于到了給靜態(tài)字段賦值的時候了规丽,無論是直接賦值還是通過靜態(tài)塊來完成蒲牧,編譯器都會把這些賦值語句收集到一起撇贺,并且按程序書寫的順序赌莺,然后放在一個叫clinit
的方法中,依次執(zhí)行松嘶。這個動作艘狭,我們稱為初始化。
類加載進階
以上是針對類加載機制的一個簡單介紹翠订,下面我們進行一些更加高階的講解巢音。
一種優(yōu)雅的單例實現(xiàn)
實現(xiàn)單例有多種不同的寫法,也個有優(yōu)缺點尽超,其中“餓漢式”的寫法最為簡潔官撼,但缺點是一旦觸發(fā)了類加載就會同步進行實例化。觸發(fā)的機制前面的基礎篇中已經(jīng)提到過似谁,比如調(diào)用Resource中存在的任意一個static字段或者方法傲绣,就會觸發(fā)Resource類的類加載。
而下面的寫法通過引入靜態(tài)內(nèi)部類完美的解決了這個問題巩踏。結(jié)合剛才提到的類加載機制秃诵,說說這是為什么?
public class Resource {
private Resource() {}
public static Resource getInstance() {
return Holder.resource;
}
private static class Holder {
public static final Resource resource = new Resource();
}
}
關于雙親委派模型
剛才介紹了幾個不同的類加載器塞琼,那么他們之間是怎樣合作的菠净?我們結(jié)合ClassLoader類中的loadClass方法來看:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,檢查該類是否已經(jīng)被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
// 針對未加載過的類彪杉,先嘗試讓父類加載器進行加載
try {
// 啟動類加載器是通過C++實現(xiàn)的毅往,只能表示為null
// 因此這里有2個邏輯分支
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 返回一個啟動類加載器加載的類,如果沒有則返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 如果父類加載器無法加載派近,再嘗試自己加載
if (c == null) {
c = findClass(name);
}
}
// 其他實現(xiàn)細節(jié)...
return c;
}
}
通過自定義類加載器實現(xiàn)熱部署
熱部署指的是不需要重啟應用就可以動態(tài)的替換掉其中的一些功能煞抬,類加載器給我們提供了這樣一種實現(xiàn)的思路。
首先构哺,我們說在Java的世界里革答,通過一個類的全限定名 + 類加載器战坤,可以唯一的定位一個類,也就是說残拐,哪怕是同一個.class文件途茫,通過不同的類加載器進入JVM,它們之間也是互相隔離的溪食。
基于上述事實囊卜,當我們希望只是升級某個類的功能時,就可以通過這樣的機制來實現(xiàn):為該類實例化一個新的類加載器错沃,并重新加載該類栅组,最后替換掉之前舊的版本。
根據(jù)這樣的思路枢析,我們可以定義一個MyTest類玉掸,擁有一個showVersion()方法,在第一個版本中會打印1.0醒叁,在第二個版本中會打印2.0司浪,代表功能進行了升級。
public class MyTest {
public void showVersion() {
System.out.println("1.0版本");
}
}
接著把沼,需要自定義一個類加載器啊易,重寫部分方法,簡單來說饮睬,它會根據(jù)類的全限定名租谈,在/tmp目錄下找對應的字節(jié)碼文件,針對特定的類捆愁,如MyTest割去,不經(jīng)過雙親委派模型,直接加載進內(nèi)存中牙瓢。
public class MyClassLoader extends ClassLoader {
// 指定那些類可以通過自定義類加載器的方式加載
private Set<String> classNamesLoadMyself = new HashSet<>();
public MyClassLoader(String ... classNames) {
for (String className : classNames) {
classNamesLoadMyself.add(className);
}
}
@Override
protected Class<?> findClass(String name) {
// 根據(jù)路徑和類名找到對應的文件并轉(zhuǎn)化為相應的字節(jié)流
byte[] bytes = FileUtil.getClassByte("/tmp", name);
return defineClass(name, bytes, 0, bytes.length);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 如果是指定了要自定義類加載的類劫拗,則繞開雙親委派模型
if (classNamesLoadMyself.contains(name)) {
return findClass(name);
}
return super.loadClass(name);
}
}
最后,我們對這個自定義的類加載器做一個測試矾克。
public class MyClassLoaderClient {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
// 實例化一個類加載器
MyClassLoader myClassLoader = new MyClassLoader("MyTest");
// 注意這里不能直接強制類型轉(zhuǎn)化為MyTest
Object myTest = myClassLoader.loadClass(className).newInstance();
myTest.getClass().getMethod("showVersion").invoke(myTest);
// 休眠1秒
TimeUnit.SECONDS.sleep(1);
}
}
}
在這個測試類中有一行注釋页慷,不能將實例化的MyTest做強制類型轉(zhuǎn)換,請問這是為什么呢胁附?