JVM類加載機(jī)制
前不久實(shí)習(xí)面試被問到了JVM類加載機(jī)制桩蓉,回答的比較差,近幾天又看了些相關(guān)的內(nèi)容郭赐,所以打算寫個(gè)博客記錄下來救欧。本文的主要內(nèi)容源自于[深入理解Java虛擬機(jī)][1]纸肉、[IBM類加載器的文檔][2]以及一些優(yōu)秀的博客蹲盘。
概述
在Java語言中,類加載鏈接過程都是在程序運(yùn)行的時(shí)候完成的,換言之就是動態(tài)加載和動態(tài)連接困介,這使Java可以動態(tài)擴(kuò)展,同樣也帶來了一定的性能開銷蘸际。
類加載過程
類的生命周期主要分為七個(gè)階段:加載座哩、連接(驗(yàn)證、準(zhǔn)備粮彤、解析)根穷、初始化、使用以及卸載导坟。其中屿良,只有加載、驗(yàn)證惫周、準(zhǔn)備尘惧、初始化和卸載這五個(gè)過程順序是確定的,必須按照這個(gè)順序開始递递,但是這些階段總是相互交叉地混合式進(jìn)行喷橙。
加載
在加載過程中,JVM主要完成三項(xiàng)工作:
通過類的全限定名獲取類的二進(jìn)制流
將靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
-
在堆中生成代表這個(gè)類的Class對象登舞,作為訪問入口
需要注意的是贰逾,這里并沒有規(guī)定如何去獲取二進(jìn)制流,我認(rèn)為這也是類加載器可以重載的主要原因菠秒。
驗(yàn)證
這一過程主要就是保證Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求疙剑,并且不會危害虛擬機(jī)自身的安全。這一過程主要包括四個(gè)階段:**文件格式驗(yàn)證**践叠、**元數(shù)據(jù)驗(yàn)證**言缤、**字節(jié)碼驗(yàn)證**和**符號引用驗(yàn)證**。
文件格式驗(yàn)證
第一部分主要就是**驗(yàn)證字節(jié)流是否符合Class文件的規(guī)范并且能夠被當(dāng)前版本的虛擬機(jī)處理**酵熙。比如是否已0xCAFEBABE開頭轧简、主次版本是否符合虛擬機(jī)要求、指向常量值的索引是否合法等匾二。
元數(shù)據(jù)驗(yàn)證
第二部分主要是**對字節(jié)碼描述的信息進(jìn)行語義分析哮独,保證其符合Java語言規(guī)范的要求**。比如是否有類察藐、父類是否可以被繼承皮璧、若不是抽象類,是否實(shí)現(xiàn)了所有方法等分飞。
字節(jié)碼驗(yàn)證
第三部分主要是**進(jìn)行數(shù)據(jù)流和控制流的分析**悴务。其實(shí)這一部分跟編譯原理上學(xué)到的內(nèi)容比較相似,也是真?zhèn)€過程中最復(fù)雜的一部分,在JDK 1.6之后讯檐,增加了一個(gè)名為“StackMapTable”的屬性來減少這一過程所使用的時(shí)間羡疗。比如保證類型轉(zhuǎn)換是有效的、保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上等别洪。
符號引用驗(yàn)證
最后一個(gè)階段發(fā)生在**虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時(shí)候**叨恨,這個(gè)轉(zhuǎn)化動作將在解析過程中發(fā)生。比如通過符號引用中的描述是否可以找到對應(yīng)的類挖垛、指定類中是否含有符合方法的字段描述符及簡單名稱所表述的方法和字段痒钝。
整個(gè)驗(yàn)證過程,我還是認(rèn)為是非常復(fù)雜痢毒,尤其是字節(jié)碼驗(yàn)證過程是很難實(shí)現(xiàn)的送矩。
準(zhǔn)備
準(zhǔn)備階段則是正式為**類變量**在方法區(qū)分配內(nèi)存并設(shè)置**類變量**初始值的階段。這里需要注意到主要有:
- 非常量字段會被初始化“零值”
- 常量字段會被初始化常量值
解析
解析階段是**虛擬機(jī)將常量池內(nèi)符號引用替換為直接引用的過程**哪替。虛擬機(jī)規(guī)范并未規(guī)定解析階段發(fā)生的具體時(shí)間栋荸,只要求在執(zhí)行13個(gè)用于操作符號引用的字節(jié)碼指令之前,先對它們所使用的符號引用進(jìn)行解析夷家。
- 符號引用:符號引用以一組符號來描述所引用的目標(biāo)蒸其,符號可以是任意形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可库快。
- 直接引用:直接引用是直接指向目標(biāo)的指針摸袁、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。
解析動作主要針對類或接口(CONSTANT_Class_info)义屏、字段(CONSTANT_Fieldref_info)靠汁、類方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)四類闽铐。
初始化
除了用戶可以采用自定義類加載器參與之外蝶怔,前面所有的過程都是由虛擬機(jī)主導(dǎo)和控制的。到了初始化階段兄墅,才真正意義上執(zhí)行類中定義的Java程序代碼踢星,也就是<clinit>類創(chuàng)建函數(shù)中的內(nèi)容。<clinit>函數(shù)則是由編譯器自動收集類中的所有的類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的隙咸。
我認(rèn)為在這一過程中沐悦,有以下幾點(diǎn)是需要注意的:
靜態(tài)語句塊中可以訪問、賦值定義在靜態(tài)語句塊之前的變量五督,定義在它之后的變量只可以賦值藏否。
虛擬機(jī)保證在子類<clinit>()方法執(zhí)行之前,父類的<clinit>()一定已經(jīng)執(zhí)行完畢充包,這也就意味著第一個(gè)執(zhí)行初始化的類一定是Object類
如果一個(gè)類中并不存在靜態(tài)語句塊副签,也不存在對變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法
只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會被初始化(對于接口的實(shí)現(xiàn)類也是一樣)淆储。
-
<clinit>是線程安全操作冠场,不要在<clinit>中使用耗時(shí)很久的操作
在Java虛擬機(jī)規(guī)范中強(qiáng)制性的規(guī)定了如果一個(gè)類未初始化時(shí)必須初始化的四種情況:
遇到new、getstatic遏考、putstatic或invokestatic四條字節(jié)碼(實(shí)例化對象慈鸠、靜態(tài)字段以及靜態(tài)方法)
使用反射時(shí)
初始化子類時(shí),要先初始化父類
-
包含main函數(shù)的類
需要注意三種情況是不會引起類初始化:
通過子類引用父類的靜態(tài)字段灌具,不會導(dǎo)致子類初始化
數(shù)組引用不會導(dǎo)致類初始化
-
引用常量不會引起類初始化
對于第一種情況,從字節(jié)碼上看確實(shí)是調(diào)用了getsatic字節(jié)碼譬巫,不過從輸出結(jié)果上看咖楣,的確沒有子類信息的初始化,這一部分我在我在網(wǎng)絡(luò)上也沒有找到解釋芦昔,等以后有時(shí)間再填坑诱贿。
public class testParentStatic {
public static void main(String[] args) {
System.out.print(SubClass.i);
}
}
class SuperClass {
static {
System.out.println("SuperClass <clinit>");
}
public static int i = 50;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass <clinit>");
}
}
輸出結(jié)果為:
SuperClass <clinit>
50
...
#3 = Fieldref #23.#24 // SubClass.i:I
...
#23 = Class #32 // SubClass
#24 = NameAndType #33:#34 // i:I
...
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field SubClass.i:I
6: invokevirtual #4 // Method java/io/PrintStream.print:(I)V
...
對于第二部分就很好解釋了,在創(chuàng)建數(shù)組時(shí)咕缎,字節(jié)碼為 **newarray** 珠十,如 **new int[20]** ,這是初始化的類為 **[I**凭豪。其實(shí)Java的數(shù)組類是動態(tài)創(chuàng)建了特殊的類焙蹭,其中并沒有**length**等字段,都是通過 **arraylength** 等字節(jié)碼由JVM實(shí)現(xiàn)的嫂伞。
對于第三種情況孔厉,常量字段是存儲在常量池中,并不會使用符號引用作為入口帖努,當(dāng)然也不會使類初始化了撰豺。
與類不同,接口只存在于一種情況:
- 一個(gè)接口初始化時(shí)拼余,并不要求其父接口全部完成初始化污桦,只有使用了父接口的成員時(shí)才會初始化
卸載
對于使用過程,是一個(gè)比較熟悉的部分了匙监,在此就不再贅述了凡橱,再談一談?lì)惿芷诘男遁d過程。類卸載舅柜,我認(rèn)為本質(zhì)上講就是GC對方法區(qū)(也就是所謂的永生代)的類數(shù)據(jù)進(jìn)行垃圾回收梭纹。根據(jù)Java虛擬機(jī)規(guī)范,只有**無用的類**才可以被回收致份,這需要滿足三個(gè)條件:
該類所有的實(shí)例都已經(jīng)被回收变抽,即Java堆中不存在該類的任何實(shí)例;
加載該類的CLassLoader(實(shí)例)已經(jīng)被回收;
-
該類對應(yīng)的Class對象沒有在任何地方被引用绍载,無法在任何地方通過反射訪問該類的方法诡宗。
引導(dǎo)類加載器實(shí)例永遠(yuǎn)為reachable狀態(tài),有引導(dǎo)類加載器加載的對象理論上說應(yīng)該永遠(yuǎn)不會被卸載击儡。其實(shí)塔沃,我認(rèn)JVM默認(rèn)提供的三種類加載器加載的類應(yīng)該都是不會被回收的,只有用戶自定義的類加載器才會被回收阳谍。
當(dāng)然滿足上述三個(gè)條件的無用的類也只是可以被回收蛀柴,至于會不會回收,什么時(shí)候回收都還不一定的(關(guān)于GC矫夯,深入理解Java虛擬機(jī)已經(jīng)比較詳細(xì)了鸽疾,這里就不說了)。
類加載器
類加載器是Java中的一個(gè)核心功能训貌,通過類加載器實(shí)現(xiàn)類加載階段的“通過一個(gè)類的全限定名來獲取表述此類的二進(jìn)制字節(jié)流”制肮。在Java中有三種主要的預(yù)定義類型類加載器,當(dāng)JVM啟動時(shí)递沪,Java默認(rèn)使用這三種類加載器(這一部分名稱以[IBM文檔][2]為準(zhǔn)):
引導(dǎo)加載器:負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的豺鼻,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(文件名識別)Java核心庫加載到虛擬機(jī)內(nèi)存中款慨。采用原生代碼實(shí)現(xiàn)儒飒,并不繼承自ClassLoader。由于引導(dǎo)類加載器涉及到虛擬機(jī)的本地實(shí)現(xiàn)細(xì)節(jié)樱调,因此開發(fā)者無法直接獲取到啟動類加載器的引用约素,不允許直接通過引用進(jìn)行操作。
擴(kuò)展類加載器:負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的笆凌,或者被java.ext.dirs系統(tǒng)變量所制定的路徑下的所有類庫圣猎。
-
系統(tǒng)類加載器:負(fù)責(zé)加載用戶類路徑(CLASSPATH)上指定的類庫,一般情況下這個(gè)就是程序中默認(rèn)的加載器乞而∷突冢可以通過ClassLoader.getSystemClassLoader()獲取其引用。
其實(shí)還有線程上下文加載器爪模,這個(gè)將在后面單獨(dú)介紹欠啤。
雙親委派模型
以上三個(gè)類加載器實(shí)際上都是滿足一定的層次關(guān)系的,這種關(guān)系稱為雙親委派模型屋灌。雙親委派模型要求除了頂層啟動類加載器外洁段,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里的父子關(guān)系一般不會以繼承關(guān)系來實(shí)現(xiàn)的共郭,而是使用組合關(guān)系來復(fù)用父加載器的代碼祠丝。通俗的講疾呻,就是某個(gè)特定的類加載器在接到加載類的請求時(shí),首先將加載任務(wù)委托給父類加載器写半,依次遞歸岸蜗,如果父類加載器可以完成類加載任務(wù),就成功返回叠蝇;只有父類加載器無法完成此加載任務(wù)時(shí)璃岳,才自己去加載。
在**ClassLoader**類中有四個(gè)方法尤為重要悔捶,下面看下這四個(gè)方法的簡要介紹:
// 加載指定全限定名的二進(jìn)制類型铃慷,這是供用戶使用的接口
public Class<?> loadClass(String name) throws ClassNotFoundException{...}
// resolve表示是否解析,主要供繼承使用
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{...}
// loadClass中使用的類載入方法蜕该,供繼承用
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 定義類型枚冗,在findClass方法中讀取到對應(yīng)字節(jié)碼后調(diào)用,JVM已經(jīng)實(shí)現(xiàn)了對應(yīng)的功能蛇损,解析相應(yīng)的字節(jié)碼,產(chǎn)生相應(yīng)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)放置到方法區(qū)坛怪,不可繼承
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError{...}
在擴(kuò)展加載器器和系統(tǒng)加載器中**loadClass**方法使用的都是與父類**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 {
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;
}
}
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class<?> findBootstrapClass(String name);
這部分代碼邏輯十分清晰,首先檢查類是否已經(jīng)被加載袜匿,若沒有加載則調(diào)用父加載器的**loadClass()**方法更啄,若父加載器為空則默認(rèn)使用啟動類加載器作為父加載器。如果父加載器加載失敗居灯,則在拋出異常后祭务,調(diào)用自己的**findClass()**方法進(jìn)行加載。需要提及的是**ClassLoader**的**loadClass()**方法如果不被子類復(fù)寫是線程安全方法怪嫌。
這就帶來了一種優(yōu)先級關(guān)系义锥。這也就是雙親委托機(jī)制帶來的好處所在了,真正完成類的加載工作的類加載器和啟動這個(gè)加載過程的類加載器是可以不是同一個(gè)岩灭。真正完成類的加載工作是通過調(diào)用**defineClass**來實(shí)現(xiàn)的拌倍;而啟動類的加載過程是通過調(diào)用**loadClass**來實(shí)現(xiàn)的。前者稱為一個(gè)類的定義加載器噪径,后者稱為初始加載器柱恤。在JVM判斷兩個(gè)類是否相同的時(shí)候,使用的是類的定義加載器(對于任意一個(gè)類找爱,都需要由加載它的類和這個(gè)類本身一同確定其在JVM中的唯一性梗顺,不同加載器加載的類被置于不同的命名空間之中)。比如Object類车摄,它存放在**rt.jar**之中寺谤,無論哪一個(gè)類加載器要加載這個(gè)類仑鸥,最終都是委派給引導(dǎo)加載器進(jìn)行加載,它們總是同一個(gè)類矗漾。
public class testParentClassLoader {
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出結(jié)果為:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
在這里锈候,我們可以判定系統(tǒng)加載器的父加載器是擴(kuò)展加載器,但是擴(kuò)展加載器的父加載器為null敞贡,但是我們注意到當(dāng)調(diào)用其父類時(shí)泵琳,采用的native本地方法,這便是調(diào)用了引導(dǎo)加載器方法誊役,同時(shí)也未在Java文件中獲取相應(yīng)的引用获列。
上文中提到了采用反射的方式也可以是類初始化,所以采用反射的方式創(chuàng)建類的實(shí)例一定會有類的載入這一過程蛔垢,我們觀察下代碼:
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
很顯然我們可以看出來击孩,在一個(gè)類中采用**Class.forName(String name)**的方式創(chuàng)建一個(gè)類的實(shí)例默認(rèn)是采用調(diào)用類的加載器來進(jìn)行加載,當(dāng)然也可以采用具有類加載器參數(shù)的方法進(jìn)行創(chuàng)建鹏漆。
破壞雙親委派模型
上文中提到過雙親委派模型并不是一個(gè)強(qiáng)制性的約束模型巩梢,而是Java設(shè)計(jì)者們推薦給開發(fā)者們的類加載器的實(shí)現(xiàn)方式。到目前為止艺玲,主要主要出現(xiàn)過三次較大規(guī)模的“破壞”情況括蝠。
雙親委派模型之前
第一次破壞發(fā)生在雙親委派模型之前,為了兼容以前的代碼在這之后的**ClassLoader**增加了一個(gè)新方法**findClass()**饭聚,在此之前用戶只通過**loadClass()**實(shí)現(xiàn)自定義類加載器忌警。在JDK 1.2之后,已經(jīng)不再提倡采用覆蓋**loadClass()**秒梳,而應(yīng)當(dāng)把自己的類加載邏輯寫到**findClass()**方法完成加載法绵,這樣可以保證新寫出來的類加載器是符合雙親委派模型的。
線程上下文加載器
雙親委派模型本身是存在著缺陷的酪碘,無法解決基礎(chǔ)類調(diào)用回用戶代碼的情況朋譬。很典型的例子就是JNDI服務(wù),它的代碼由引導(dǎo)類加載器去加載婆跑,但JNDI的目的就是對資源進(jìn)行管理和查找此熬,它需要調(diào)用由獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序**CLASSPATH**下的JNDI接口提供者(SPI)的代碼。
在Java中采用線程上下文加載器解決這一問題滑进,如果不進(jìn)行額外的設(shè)置犀忱,那么線程上下文加載器就是系統(tǒng)上下文加載器。在SPI接口是使用線程上下文加載器扶关,就可以成功加載到SPI實(shí)現(xiàn)的類阴汇。
當(dāng)然,使用線程上下文加載類节槐,也需要注意保證多個(gè)需要通信的線程間類加載器應(yīng)該是同一個(gè)搀庶,防止因?yàn)轭惣虞d器示例不同而導(dǎo)致類型不同拐纱。
在JDK中,**URLClassLoader**配合**findClass**方法使用**defineClass**(這里的**defineClass**方法與上文提到有所不同)實(shí)現(xiàn)從網(wǎng)絡(luò)或者硬盤上加載class文件哥倔。先簡單看下秸架,**URLClassLoader**的繼承關(guān)系:
public class URLClassLoader extends SecureClassLoader {...}
public class SecureClassLoader extends ClassLoader {...}
現(xiàn)在我們再仔細(xì)看下URLClassLoader 和SecureClassLoader中的各種defineClass方法:
//SecureClassLoader:
protected final Class<?> defineClass(String name,
byte[] b, int off, int len,
CodeSource cs)
{
return defineClass(name, b, off, len, getProtectionDomain(cs));
}
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
CodeSource cs)
{
return defineClass(name, b, getProtectionDomain(cs));
}
//URLClassLoader:
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
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;
}
實(shí)際上,每一層都對**defineClass**進(jìn)行了一次封裝咆蒿,通過每一層的解析最終轉(zhuǎn)換成了最終的模式东抹。
如何選擇類加載器?
如果代碼是限于某些特定框架沃测,這些框架有著特定的加載規(guī)則缭黔,則不需要做任何改動,讓框架開發(fā)者來保證其工作蒂破。再其他情況馏谨,我們可以自己選擇最合適的類加載器,可以使用策略模式來設(shè)計(jì)選擇機(jī)制附迷。其思想將“總是使用上下文加載器”或者“總是使用當(dāng)前類加載器”的決策同具體邏輯分離開惧互。以下是參考博客使用的策略方式,應(yīng)該可以適應(yīng)大部分的工作場景:
/**
* 類加載上下文喇伯,持有要加載的類
*/
public class ClassLoadContext {
private final Class m_caller;
public final Class getCallerClass() {
return m_caller;
}
ClassLoadContext(final Class caller) {
m_caller = caller;
}
}
/**
* 類加載策略接口
*/
public interface IClassLoadStrategy {
ClassLoader getClassLoader(ClassLoadContext ctx);
}
/**
* 缺省的類加載策略壹哺,可以適應(yīng)大部分工作場景
*/
public class DefaultClassLoadStrategy implements IClassLoadStrategy {
/**
* 為ctx返回最合適的類加載器,從系統(tǒng)類加載器艘刚、當(dāng)前類加載器
* 和當(dāng)前線程上下文類加載中選擇一個(gè)最底層的加載器
* @param ctx
* @return
*/
@Override
public ClassLoader getClassLoader(final ClassLoadContext ctx) {
final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader result;
// If 'callerLoader' and 'contextLoader' are in a parent-child
// relationship, always choose the child:
if (isChild(contextLoader, callerLoader)) {
result = callerLoader;
} else if (isChild(callerLoader, contextLoader)) {
result = contextLoader;
} else {
// This else branch could be merged into the previous one,
// but I show it here to emphasize the ambiguous case:
result = contextLoader;
}
final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
// Precaution for when deployed as a bootstrap or extension class:
if (isChild(result, systemLoader)) {
result = systemLoader;
}
return result;
}
// 判斷anotherLoader是否是oneLoader的child
private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
//...
}
// ... more methods
}
決定應(yīng)該使用何種類加載器的接口是**ClassLoaderStrategy**,為了幫助**IClassLoaderStrategy**做決定截珍,給它傳遞了個(gè)**ClassLoadContext**對象作為參數(shù)攀甚,**ClassLoadContext**持有要加載的類。
上面的代碼邏輯十分清晰:如果調(diào)用類的當(dāng)前類加載器和上下文類加載器是父子關(guān)系岗喉,則總選擇自類加載器秋度。對子類加載器可見的資源通常是對父類可見資源的超集,因此如果每個(gè)開發(fā)者都遵循代理規(guī)則钱床,這樣做大多數(shù)情況下是合適的荚斯。
如果當(dāng)前類加載器和上下文類加載器是兄弟關(guān)系時(shí),決定使用哪一個(gè)是比較困難的查牌。理想情況下事期,Java運(yùn)行時(shí)不應(yīng)產(chǎn)生這種模糊。但一旦發(fā)生纸颜,上面代碼選擇上下文類加載器(參考博主的實(shí)際經(jīng)驗(yàn))兽泣。**一般來說,上下文類加載器要比當(dāng)前類加載器更適合于框架編程胁孙,而當(dāng)前類加載器則更適合于業(yè)務(wù)邏輯編程唠倦。**最后需要檢查一下称鳞,以便保證所選類加載器不是系統(tǒng)類加載器的父親,在開發(fā)標(biāo)準(zhǔn)擴(kuò)展類庫時(shí)這通常是個(gè)好習(xí)慣稠鼻。
代碼熱替換冈止、熱部署
實(shí)際上就是希望應(yīng)用程序能夠像我們的電腦外設(shè)那樣,插上鼠標(biāo)或U盤候齿,不用重啟就能夠立即使用熙暴,鼠標(biāo)有問題或者升級就換個(gè)鼠標(biāo),不同停機(jī)也不用重啟毛肋。對于個(gè)人電腦來說怨咪,重啟一次沒什么大不了的,但對于一些生產(chǎn)系統(tǒng)來說润匙,關(guān)機(jī)重啟一次可能就要被列為生產(chǎn)事故诗眨,這種情況熱部署對于軟件開發(fā)者,尤其是企業(yè)級軟件開發(fā)者具有很大的吸引力孕讳。
OSGi是當(dāng)前業(yè)界Java模塊化標(biāo)準(zhǔn)匠楚,而OSGi實(shí)現(xiàn)模塊化熱部署的關(guān)鍵則是它自定義的類加載器機(jī)制的實(shí)現(xiàn)。每一個(gè)程序模塊(Bundle)都有一個(gè)自己的類加載器厂财,當(dāng)需要更換一個(gè)Bundle時(shí)芋簿,就把Bundle連同類加載器一起換掉以實(shí)現(xiàn)代碼的熱替換。
在OSGi環(huán)境中璃饱,類加載器不再是雙親委托模型的樹狀結(jié)構(gòu)与斤,而是進(jìn)一步發(fā)展為網(wǎng)狀結(jié)構(gòu),當(dāng)收到類加載請求時(shí)荚恶,OSGi將按照下面的順序進(jìn)行類搜索:
將以java.*開頭的類撩穿,委托給父類加載器加載
否則,將委派列表名單內(nèi)的類谒撼,委派給父類加載器加載
否則食寡,將Import列表中的類,委派給Export這個(gè)類的Bundle的類加載器加載
否則廓潜,查找類是否在自己的Fragment Bundle中抵皱,如果在,則委派給Fragment Bundle的類加載器加載
-
否則辩蛋,類查找失敗
上面的查找順序中只有開頭兩點(diǎn)仍然符合雙親委派規(guī)則呻畸,其余的類查找都是在平級的類加載器中進(jìn)行的。
其實(shí)悼院,對于OGSi我并沒有怎么使用過擂错,也不是很了解,所以在這里就不詳細(xì)的介紹了樱蛤,等我什么時(shí)候有時(shí)間了解了以后可能會水篇博客钮呀。
代碼熱替代的簡單實(shí)現(xiàn)
所謂熱替代剑鞍,通俗的說就是指一個(gè)類已經(jīng)被一個(gè)加載器加載以后,在不卸載它的情況下重新加載它一次爽醋。實(shí)際上蚁署,為了實(shí)現(xiàn)這一功能必須在加載的時(shí)候進(jìn)行新的處理,先判斷是否已經(jīng)加載蚂四,若是則重新加載一次光戈,否則直接首次加載它。首先介紹下**ClassLoader**類和熱替換有關(guān)的一些方法:
findLoadedClass:每個(gè)類加載器都會維護(hù)有自己的一份已加載類名字空間遂赠,其中不能出現(xiàn)兩個(gè)同名類久妆。凡是通過該類加載器加載的類,無論是直接還是間接跷睦,都是保存在自己的名字空間中筷弦,該方法就是在該名字空間中,該方法就是在改名字空間中尋找指定的類是否已存在抑诸,如果存在就返回類的引用烂琴,否則返回null。
getSystemClassLoader:該方法返回系統(tǒng)使用的CLassLoader蜕乡〖楸粒可以在自己定制的類加載器中通過該方法把一部分工作轉(zhuǎn)交給系統(tǒng)類加載器去處理。
defineClass:該方法是ClassLoader中的非常重要的方法层玲,它接收以字節(jié)數(shù)組表示的類字節(jié)碼号醉,并把它轉(zhuǎn)換成Class實(shí)例,該方法轉(zhuǎn)換一個(gè)類的同時(shí)辛块,會先要求裝載該類的父類以及實(shí)現(xiàn)的接口類扣癣。
loadClass:加載類的入口方法,調(diào)用該方法完成類的顯示加載憨降。通過對該方法的重新實(shí)現(xiàn),我們可以完全控制和管理類的加載過程该酗。
-
resolveClass:鏈接一個(gè)指定的類授药。這是一個(gè)在某些情況下確保類可用的必要方法。
在實(shí)現(xiàn)熱替換時(shí)需要有兩點(diǎn)進(jìn)行特別的說明:
要想實(shí)現(xiàn)同一個(gè)類的不同版本互存呜魄,那么這些不同版本必須由不同的類加載器進(jìn)行加載悔叽,那么這些不同版本必須由不同的類加載器進(jìn)行加載,因此就不能把這些類的加載工作委托給系統(tǒng)加載器爵嗅。
-
為了做到這一點(diǎn)娇澎,就不能采用系統(tǒng)默認(rèn)的類加載委托規(guī)則,換言之睹晒,我們定制的類加載器的父加載器必須設(shè)置為null趟庄。
下面是一個(gè)很簡單的官方demo:
package com.dongxi.hotswaptest;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashSet;
public class HotSwapClassLoader extends ClassLoader {
private String basedir; // 需要該類加載器直接加載的類文件的基目錄
private HashSet dynaclazns; // 需要由該類加載器直接加載的類名
public HotSwapClassLoader(String basedir, String[] clazns) throws Exception {
super(null); // 指定父類加載器為 null
this.basedir = basedir;
dynaclazns = new HashSet();
loadClassByMe(clazns);
}
private void loadClassByMe(String[] clazns) throws Exception {
for (int i = 0; i < clazns.length; i++) {
loadDirectly(clazns[i]);
dynaclazns.add(clazns[i]);
}
}
private Class loadDirectly(String name) throws Exception {
Class cls = null;
StringBuffer sb = new StringBuffer(basedir);
String classname = name.replace('.', File.separatorChar) + ".class";
sb.append(File.separator + classname);
File classF = new File(sb.toString());
cls = instantiateClass(name, new FileInputStream(classF),
classF.length());
return cls;
}
private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name, raw, 0, raw.length);
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if (!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
package com.dongxi.hotswaptest;
public class Holder {
public void sayHello() {
System.out.println("hello world! (version one)");
}
}
package com.dongxi.hotswaptest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
public class TestSwap {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (true) {
try {
HotSwapClassLoader classLoader =
new HotSwapClassLoader("C:\\Users\\22541\\IdeaProjects\\testclassloading\\target\\classes\\",
new String[]{"com.dongxi.hotswaptest.Holder"});
Class clazz = classLoader.loadClass("com.dongxi.hotswaptest.Holder");
Object holder = clazz.newInstance();
Method m = holder.getClass().getMethod("sayHello", new Class[]{});
m.invoke(holder, new Object[]{});
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
}
編譯括细、運(yùn)行我們的程序,會輸出:
hello world! (version one)
hello world! (version one)
hello world! (version one)
現(xiàn)在對**Holder**進(jìn)行修改戚啥,將其中的**version one**更改為**version two**:
package com.dongxi.hotswaptest;
public class Holder {
public void sayHello() {
System.out.println("hello world! (version two)");
}
}
重新編譯運(yùn)行奋单,我們發(fā)現(xiàn)輸出已經(jīng)發(fā)生了變化:
hello world! (version two)
hello world! (version two)
hello world! (version two)
hello world! (version two)
這里需要提及的是我們并未在測試類中使用了類型轉(zhuǎn)換(***Holder holder = (Holder)clazz.newInstance();***),這里就涉及到了我們在之前提到的JVM對類型的判定(由加載它的類和這個(gè)類本身一同確定其在JVM中的唯一性)猫十,如果進(jìn)行類型轉(zhuǎn)換那么會拋出*ClassCastException*異常览濒。這是由于*clazz*是由我們自定義的類加載器的,而*holder*變量類型和轉(zhuǎn)型的*Holder*是由run方法所屬的類加載器(系統(tǒng)加載器)進(jìn)行加載的拖云,所以會拋出異常贷笛。如果采用增加接口的方式進(jìn)行轉(zhuǎn)換,那么也是不可以的宙项,原因也大致相同乏苦。
擴(kuò)展
在運(yùn)行時(shí)判斷系統(tǒng)類加載器加載路徑
一是可以直接調(diào)用*ClassLoader.getSystemClassLoader()*或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴(kuò)展類加載器本身都派生自*URLClassLoader*),調(diào)用*URLClassLoader*中的*getURLs()*方法可以獲取到杉允。
二是可以直接通過獲取系統(tǒng)屬性java.class.path來查看當(dāng)前類路徑上的條目信息 :*System.getProperty("java.class.path")*邑贴。
在運(yùn)行時(shí)判斷標(biāo)準(zhǔn)擴(kuò)展類加載器加載路徑
import java.net.URL;
import java.net.URLClassLoader;
/**
* Created by 22541 on 2017/5/9.
*/
public class TestClassLoaderPathHas {
public static void main(String[] args) {
try {
URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : extURLs) {
System.out.println(url);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/zipfs.jar
通過類加載器加載非類資源
ClassLoader除了用于加載類外,還可以用于加載圖片叔磷、視頻等非類資源拢驾。同樣可以采用雙親委派模型將加載資源的請求傳遞到頂層的引導(dǎo)類加載器,若失敗再逐層返回改基。
URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)
源碼上的一些小東西
對于*ClassLoader*的源碼也簡單看了下繁疤,不過比較悲傷的是很多東西都看不懂,就把我能看懂的拿出來簡單說下秕狰,等以后能看懂了再來填這個(gè)坑稠腊。
前文中提到了**loadClass**方法是線程安全的,該方法是通過對**getClassLoadingLock**方法返回的Object完成的鸣哀,我們就先來看看這個(gè)方法:
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
我們可以看到這里有一個(gè)變量名為**parallelLockMap**架忌,如果這個(gè)變量為空,那么就鎖定當(dāng)前實(shí)例我衬,如果不為空叹放,那么則通過**putIfAbsent(className, newLock);**方法來獲得一個(gè)Object實(shí)例,這個(gè)方法的功能也跟名字一樣挠羔,在key不存在的時(shí)候加入一個(gè)值,如果key存在就不放入井仰,它的實(shí)現(xiàn)代碼為:
V v = map.get(key);
if (v == null)
v = map.put(key, value);
return v;
我們在轉(zhuǎn)到**ClassLoader**的構(gòu)造函數(shù),這里有**parallelLockMap**變量初始化的過程:
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
我們可以看到構(gòu)造函數(shù)根據(jù)**ParallelLoaders.isRegistered()**來給**parallelLockMap**賦值破加,**ParallelLoaders**是**ClassLoader**中的一個(gè)靜態(tài)內(nèi)部類俱恶,該類封裝了并行的可裝載類型的集合:
private static class ParallelLoaders {
private ParallelLoaders() {}
// the set of parallel capable loader types
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}
/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}
/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
在ClassLoader中通過這個(gè)類來指定并行能力,如果當(dāng)前的加載器具有并行能力,那么在根據(jù)類的名稱返回一個(gè)Object作為鎖合是,如果不具有并行能力了罪,那就不用去創(chuàng)建這些東西了,直接把該實(shí)例鎖了就可以了端仰,就醬捶惜。
結(jié)語
這篇文章由于我本身對類加載機(jī)制也不是分的了解,肯定還有很多的不足荔烧,也留了一些坑等著以后填吱七,感覺要學(xué)的東西好多的說。