JVM類加載機(jī)制
前不久實(shí)習(xí)面試被問(wèn)到了JVM類加載機(jī)制,回答的比較差,近幾天又看了些相關(guān)的內(nèi)容,所以打算寫(xiě)個(gè)博客記錄下來(lái)。本文的主要內(nèi)容源自于[深入理解Java虛擬機(jī)][1]、[IBM類加載器的文檔][2]以及一些優(yōu)秀的博客。
概述
在Java語(yǔ)言中,類加載鏈接過(guò)程都是在程序運(yùn)行的時(shí)候完成的,換言之就是動(dòng)態(tài)加載和動(dòng)態(tài)連接,這使Java可以動(dòng)態(tài)擴(kuò)展,同樣也帶來(lái)了一定的性能開(kāi)銷。
類加載過(guò)程
類的生命周期主要分為七個(gè)階段:加載、連接(驗(yàn)證、準(zhǔn)備、解析)、初始化、使用以及卸載。其中,只有加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)過(guò)程順序是確定的,必須按照這個(gè)順序開(kāi)始,但是這些階段總是相互交叉地混合式進(jìn)行。
加載
在加載過(guò)程中,JVM主要完成三項(xiàng)工作:
通過(guò)類的全限定名獲取類的二進(jìn)制流
將靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
-
在堆中生成代表這個(gè)類的Class對(duì)象,作為訪問(wèn)入口
需要注意的是,這里并沒(méi)有規(guī)定如何去獲取二進(jìn)制流,我認(rèn)為這也是類加載器可以重載的主要原因。
驗(yàn)證
這一過(guò)程主要就是保證Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。這一過(guò)程主要包括四個(gè)階段:**文件格式驗(yàn)證**、**元數(shù)據(jù)驗(yàn)證**、**字節(jié)碼驗(yàn)證**和**符號(hào)引用驗(yàn)證**。
文件格式驗(yàn)證
第一部分主要就是**驗(yàn)證字節(jié)流是否符合Class文件的規(guī)范并且能夠被當(dāng)前版本的虛擬機(jī)處理**。比如是否已0xCAFEBABE開(kāi)頭、主次版本是否符合虛擬機(jī)要求、指向常量值的索引是否合法等。
元數(shù)據(jù)驗(yàn)證
第二部分主要是**對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,保證其符合Java語(yǔ)言規(guī)范的要求**。比如是否有類、父類是否可以被繼承、若不是抽象類,是否實(shí)現(xiàn)了所有方法等。
字節(jié)碼驗(yàn)證
第三部分主要是**進(jìn)行數(shù)據(jù)流和控制流的分析**。其實(shí)這一部分跟編譯原理上學(xué)到的內(nèi)容比較相似,也是真?zhèn)€過(guò)程中最復(fù)雜的一部分,在JDK 1.6之后,增加了一個(gè)名為“StackMapTable”的屬性來(lái)減少這一過(guò)程所使用的時(shí)間。比如保證類型轉(zhuǎn)換是有效的、保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上等。
符號(hào)引用驗(yàn)證
最后一個(gè)階段發(fā)生在**虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候**,這個(gè)轉(zhuǎn)化動(dòng)作將在解析過(guò)程中發(fā)生。比如通過(guò)符號(hào)引用中的描述是否可以找到對(duì)應(yīng)的類、指定類中是否含有符合方法的字段描述符及簡(jiǎn)單名稱所表述的方法和字段。
整個(gè)驗(yàn)證過(guò)程,我還是認(rèn)為是非常復(fù)雜,尤其是字節(jié)碼驗(yàn)證過(guò)程是很難實(shí)現(xiàn)的。
準(zhǔn)備
準(zhǔn)備階段則是正式為**類變量**在方法區(qū)分配內(nèi)存并設(shè)置**類變量**初始值的階段。這里需要注意到主要有:
- 非常量字段會(huì)被初始化“零值”
- 常量字段會(huì)被初始化常量值
解析
解析階段是**虛擬機(jī)將常量池內(nèi)符號(hào)引用替換為直接引用的過(guò)程**。虛擬機(jī)規(guī)范并未規(guī)定解析階段發(fā)生的具體時(shí)間,只要求在執(zhí)行13個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們所使用的符號(hào)引用進(jìn)行解析。
- 符號(hào)引用:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任意形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。
- 直接引用:直接引用是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。
解析動(dòng)作主要針對(duì)類或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、類方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)四類。
初始化
除了用戶可以采用自定義類加載器參與之外,前面所有的過(guò)程都是由虛擬機(jī)主導(dǎo)和控制的。到了初始化階段,才真正意義上執(zhí)行類中定義的Java程序代碼,也就是<clinit>類創(chuàng)建函數(shù)中的內(nèi)容。<clinit>函數(shù)則是由編譯器自動(dòng)收集類中的所有的類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的。
我認(rèn)為在這一過(guò)程中,有以下幾點(diǎn)是需要注意的:
靜態(tài)語(yǔ)句塊中可以訪問(wèn)、賦值定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量只可以賦值。
虛擬機(jī)保證在子類<clinit>()方法執(zhí)行之前,父類的<clinit>()一定已經(jīng)執(zhí)行完畢,這也就意味著第一個(gè)執(zhí)行初始化的類一定是Object類
如果一個(gè)類中并不存在靜態(tài)語(yǔ)句塊,也不存在對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法
只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化(對(duì)于接口的實(shí)現(xiàn)類也是一樣)。
-
<clinit>是線程安全操作,不要在<clinit>中使用耗時(shí)很久的操作
在Java虛擬機(jī)規(guī)范中強(qiáng)制性的規(guī)定了如果一個(gè)類未初始化時(shí)必須初始化的四種情況: 遇到new、getstatic、putstatic或invokestatic四條字節(jié)碼(實(shí)例化對(duì)象、靜態(tài)字段以及靜態(tài)方法)
使用反射時(shí)
初始化子類時(shí),要先初始化父類
-
包含main函數(shù)的類
需要注意三種情況是不會(huì)引起類初始化: 通過(guò)子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化
數(shù)組引用不會(huì)導(dǎo)致類初始化
-
引用常量不會(huì)引起類初始化
對(duì)于第一種情況,從字節(jié)碼上看確實(shí)是調(diào)用了getsatic字節(jié)碼,不過(guò)從輸出結(jié)果上看,的確沒(méi)有子類信息的初始化,這一部分我在我在網(wǎng)絡(luò)上也沒(méi)有找到解釋,等以后有時(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
...
對(duì)于第二部分就很好解釋了,在創(chuàng)建數(shù)組時(shí),字節(jié)碼為 **newarray** ,如 **new int[20]** ,這是初始化的類為 **[I**。其實(shí)Java的數(shù)組類是動(dòng)態(tài)創(chuàng)建了特殊的類,其中并沒(méi)有**length**等字段,都是通過(guò) **arraylength** 等字節(jié)碼由JVM實(shí)現(xiàn)的。
對(duì)于第三種情況,常量字段是存儲(chǔ)在常量池中,并不會(huì)使用符號(hào)引用作為入口,當(dāng)然也不會(huì)使類初始化了。
與類不同,接口只存在于一種情況:
- 一個(gè)接口初始化時(shí),并不要求其父接口全部完成初始化,只有使用了父接口的成員時(shí)才會(huì)初始化
卸載
對(duì)于使用過(guò)程,是一個(gè)比較熟悉的部分了,在此就不再贅述了,再談一談?lì)惿芷诘男遁d過(guò)程。類卸載,我認(rèn)為本質(zhì)上講就是GC對(duì)方法區(qū)(也就是所謂的永生代)的類數(shù)據(jù)進(jìn)行垃圾回收。根據(jù)Java虛擬機(jī)規(guī)范,只有**無(wú)用的類**才可以被回收,這需要滿足三個(gè)條件:
該類所有的實(shí)例都已經(jīng)被回收,即Java堆中不存在該類的任何實(shí)例;
加載該類的CLassLoader(實(shí)例)已經(jīng)被回收;
-
該類對(duì)應(yīng)的Class對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
引導(dǎo)類加載器實(shí)例永遠(yuǎn)為reachable狀態(tài),有引導(dǎo)類加載器加載的對(duì)象理論上說(shuō)應(yīng)該永遠(yuǎn)不會(huì)被卸載。其實(shí),我認(rèn)JVM默認(rèn)提供的三種類加載器加載的類應(yīng)該都是不會(huì)被回收的,只有用戶自定義的類加載器才會(huì)被回收。
當(dāng)然滿足上述三個(gè)條件的無(wú)用的類也只是可以被回收,至于會(huì)不會(huì)回收,什么時(shí)候回收都還不一定的(關(guān)于GC,深入理解Java虛擬機(jī)已經(jīng)比較詳細(xì)了,這里就不說(shuō)了)。
類加載器
類加載器是Java中的一個(gè)核心功能,通過(guò)類加載器實(shí)現(xiàn)類加載階段的“通過(guò)一個(gè)類的全限定名來(lái)獲取表述此類的二進(jìn)制字節(jié)流”。在Java中有三種主要的預(yù)定義類型類加載器,當(dāng)JVM啟動(dòng)時(shí),Java默認(rèn)使用這三種類加載器(這一部分名稱以[IBM文檔][2]為準(zhǔn)):
引導(dǎo)加載器:負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(文件名識(shí)別)Java核心庫(kù)加載到虛擬機(jī)內(nèi)存中。采用原生代碼實(shí)現(xiàn),并不繼承自ClassLoader。由于引導(dǎo)類加載器涉及到虛擬機(jī)的本地實(shí)現(xiàn)細(xì)節(jié),因此開(kāi)發(fā)者無(wú)法直接獲取到啟動(dòng)類加載器的引用,不允許直接通過(guò)引用進(jìn)行操作。
擴(kuò)展類加載器:負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所制定的路徑下的所有類庫(kù)。
-
系統(tǒng)類加載器:負(fù)責(zé)加載用戶類路徑(CLASSPATH)上指定的類庫(kù),一般情況下這個(gè)就是程序中默認(rèn)的加載器??梢酝ㄟ^(guò)ClassLoader.getSystemClassLoader()獲取其引用。
其實(shí)還有線程上下文加載器,這個(gè)將在后面單獨(dú)介紹。
雙親委派模型
以上三個(gè)類加載器實(shí)際上都是滿足一定的層次關(guān)系的,這種關(guān)系稱為雙親委派模型。雙親委派模型要求除了頂層啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里的父子關(guān)系一般不會(huì)以繼承關(guān)系來(lái)實(shí)現(xiàn)的,而是使用組合關(guān)系來(lái)復(fù)用父加載器的代碼。通俗的講,就是某個(gè)特定的類加載器在接到加載類的請(qǐng)求時(shí),首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無(wú)法完成此加載任務(wù)時(shí),才自己去加載。
在**ClassLoader**類中有四個(gè)方法尤為重要,下面看下這四個(gè)方法的簡(jiǎn)要介紹:
// 加載指定全限定名的二進(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方法中讀取到對(duì)應(yīng)字節(jié)碼后調(diào)用,JVM已經(jīng)實(shí)現(xiàn)了對(duì)應(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)被加載,若沒(méi)有加載則調(diào)用父加載器的**loadClass()**方法,若父加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器。如果父加載器加載失敗,則在拋出異常后,調(diào)用自己的**findClass()**方法進(jìn)行加載。需要提及的是**ClassLoader**的**loadClass()**方法如果不被子類復(fù)寫(xiě)是線程安全方法。
這就帶來(lái)了一種優(yōu)先級(jí)關(guān)系。這也就是雙親委托機(jī)制帶來(lái)的好處所在了,真正完成類的加載工作的類加載器和啟動(dòng)這個(gè)加載過(guò)程的類加載器是可以不是同一個(gè)。真正完成類的加載工作是通過(guò)調(diào)用**defineClass**來(lái)實(shí)現(xiàn)的;而啟動(dòng)類的加載過(guò)程是通過(guò)調(diào)用**loadClass**來(lái)實(shí)現(xiàn)的。前者稱為一個(gè)類的定義加載器,后者稱為初始加載器。在JVM判斷兩個(gè)類是否相同的時(shí)候,使用的是類的定義加載器(對(duì)于任意一個(gè)類,都需要由加載它的類和這個(gè)類本身一同確定其在JVM中的唯一性,不同加載器加載的類被置于不同的命名空間之中)。比如Object類,它存放在**rt.jar**之中,無(wú)論哪一個(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í)例一定會(huì)有類的載入這一過(guò)程,我們觀察下代碼:
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);
}
很顯然我們可以看出來(lái),在一個(gè)類中采用**Class.forName(String name)**的方式創(chuàng)建一個(gè)類的實(shí)例默認(rèn)是采用調(diào)用類的加載器來(lái)進(jìn)行加載,當(dāng)然也可以采用具有類加載器參數(shù)的方法進(jìn)行創(chuàng)建。
破壞雙親委派模型
上文中提到過(guò)雙親委派模型并不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者們推薦給開(kāi)發(fā)者們的類加載器的實(shí)現(xiàn)方式。到目前為止,主要主要出現(xiàn)過(guò)三次較大規(guī)模的“破壞”情況。
雙親委派模型之前
第一次破壞發(fā)生在雙親委派模型之前,為了兼容以前的代碼在這之后的**ClassLoader**增加了一個(gè)新方法**findClass()**,在此之前用戶只通過(guò)**loadClass()**實(shí)現(xiàn)自定義類加載器。在JDK 1.2之后,已經(jīng)不再提倡采用覆蓋**loadClass()**,而應(yīng)當(dāng)把自己的類加載邏輯寫(xiě)到**findClass()**方法完成加載,這樣可以保證新寫(xiě)出來(lái)的類加載器是符合雙親委派模型的。
線程上下文加載器
雙親委派模型本身是存在著缺陷的,無(wú)法解決基礎(chǔ)類調(diào)用回用戶代碼的情況。很典型的例子就是JNDI服務(wù),它的代碼由引導(dǎo)類加載器去加載,但JNDI的目的就是對(duì)資源進(jìn)行管理和查找,它需要調(diào)用由獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序**CLASSPATH**下的JNDI接口提供者(SPI)的代碼。
在Java中采用線程上下文加載器解決這一問(wèn)題,如果不進(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文件。先簡(jiǎn)單看下,**URLClassLoader**的繼承關(guān)系:
public class URLClassLoader extends SecureClassLoader {...}
public class SecureClassLoader extends ClassLoader {...}
現(xiàn)在我們?cè)僮屑?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í)際上,每一層都對(duì)**defineClass**進(jìn)行了一次封裝,通過(guò)每一層的解析最終轉(zhuǎn)換成了最終的模式。
如何選擇類加載器?
如果代碼是限于某些特定框架,這些框架有著特定的加載規(guī)則,則不需要做任何改動(dòng),讓框架開(kāi)發(fā)者來(lái)保證其工作。再其他情況,我們可以自己選擇最合適的類加載器,可以使用策略模式來(lái)設(shè)計(jì)選擇機(jī)制。其思想將“總是使用上下文加載器”或者“總是使用當(dāng)前類加載器”的決策同具體邏輯分離開(kāi)。以下是參考博客使用的策略方式,應(yīng)該可以適應(yīng)大部分的工作場(chǎ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)大部分工作場(chǎ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**對(duì)象作為參數(shù),**ClassLoadContext**持有要加載的類。
上面的代碼邏輯十分清晰:如果調(diào)用類的當(dāng)前類加載器和上下文類加載器是父子關(guān)系,則總選擇自類加載器。對(duì)子類加載器可見(jiàn)的資源通常是對(duì)父類可見(jiàn)資源的超集,因此如果每個(gè)開(kāi)發(fā)者都遵循代理規(guī)則,這樣做大多數(shù)情況下是合適的。
如果當(dāng)前類加載器和上下文類加載器是兄弟關(guān)系時(shí),決定使用哪一個(gè)是比較困難的。理想情況下,Java運(yùn)行時(shí)不應(yīng)產(chǎn)生這種模糊。但一旦發(fā)生,上面代碼選擇上下文類加載器(參考博主的實(shí)際經(jīng)驗(yàn))。**一般來(lái)說(shuō),上下文類加載器要比當(dāng)前類加載器更適合于框架編程,而當(dāng)前類加載器則更適合于業(yè)務(wù)邏輯編程。**最后需要檢查一下,以便保證所選類加載器不是系統(tǒng)類加載器的父親,在開(kāi)發(fā)標(biāo)準(zhǔn)擴(kuò)展類庫(kù)時(shí)這通常是個(gè)好習(xí)慣。
代碼熱替換、熱部署
實(shí)際上就是希望應(yīng)用程序能夠像我們的電腦外設(shè)那樣,插上鼠標(biāo)或U盤,不用重啟就能夠立即使用,鼠標(biāo)有問(wèn)題或者升級(jí)就換個(gè)鼠標(biāo),不同停機(jī)也不用重啟。對(duì)于個(gè)人電腦來(lái)說(shuō),重啟一次沒(méi)什么大不了的,但對(duì)于一些生產(chǎn)系統(tǒng)來(lái)說(shuō),關(guān)機(jī)重啟一次可能就要被列為生產(chǎn)事故,這種情況熱部署對(duì)于軟件開(kāi)發(fā)者,尤其是企業(yè)級(jí)軟件開(kāi)發(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)境中,類加載器不再是雙親委托模型的樹(shù)狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為網(wǎng)狀結(jié)構(gòu),當(dāng)收到類加載請(qǐng)求時(shí),OSGi將按照下面的順序進(jìn)行類搜索:
將以java.*開(kāi)頭的類,委托給父類加載器加載
否則,將委派列表名單內(nèi)的類,委派給父類加載器加載
否則,將Import列表中的類,委派給Export這個(gè)類的Bundle的類加載器加載
否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載
-
否則,類查找失敗
上面的查找順序中只有開(kāi)頭兩點(diǎn)仍然符合雙親委派規(guī)則,其余的類查找都是在平級(jí)的類加載器中進(jìn)行的。
其實(shí),對(duì)于OGSi我并沒(méi)有怎么使用過(guò),也不是很了解,所以在這里就不詳細(xì)的介紹了,等我什么時(shí)候有時(shí)間了解了以后可能會(huì)水篇博客。
代碼熱替代的簡(jiǎn)單實(shí)現(xiàn)
所謂熱替代,通俗的說(shuō)就是指一個(gè)類已經(jīng)被一個(gè)加載器加載以后,在不卸載它的情況下重新加載它一次。實(shí)際上,為了實(shí)現(xiàn)這一功能必須在加載的時(shí)候進(jìn)行新的處理,先判斷是否已經(jīng)加載,若是則重新加載一次,否則直接首次加載它。首先介紹下**ClassLoader**類和熱替換有關(guān)的一些方法:
findLoadedClass:每個(gè)類加載器都會(huì)維護(hù)有自己的一份已加載類名字空間,其中不能出現(xiàn)兩個(gè)同名類。凡是通過(guò)該類加載器加載的類,無(wú)論是直接還是間接,都是保存在自己的名字空間中,該方法就是在該名字空間中,該方法就是在改名字空間中尋找指定的類是否已存在,如果存在就返回類的引用,否則返回null。
getSystemClassLoader:該方法返回系統(tǒng)使用的CLassLoader。可以在自己定制的類加載器中通過(guò)該方法把一部分工作轉(zhuǎn)交給系統(tǒng)類加載器去處理。
defineClass:該方法是ClassLoader中的非常重要的方法,它接收以字節(jié)數(shù)組表示的類字節(jié)碼,并把它轉(zhuǎn)換成Class實(shí)例,該方法轉(zhuǎn)換一個(gè)類的同時(shí),會(huì)先要求裝載該類的父類以及實(shí)現(xiàn)的接口類。
loadClass:加載類的入口方法,調(diào)用該方法完成類的顯示加載。通過(guò)對(duì)該方法的重新實(shí)現(xiàn),我們可以完全控制和管理類的加載過(guò)程。
-
resolveClass:鏈接一個(gè)指定的類。這是一個(gè)在某些情況下確保類可用的必要方法。
在實(shí)現(xiàn)熱替換時(shí)需要有兩點(diǎn)進(jìn)行特別的說(shuō)明:
要想實(shí)現(xiàn)同一個(gè)類的不同版本互存,那么這些不同版本必須由不同的類加載器進(jìn)行加載,那么這些不同版本必須由不同的類加載器進(jìn)行加載,因此就不能把這些類的加載工作委托給系統(tǒng)加載器。
-
為了做到這一點(diǎn),就不能采用系統(tǒng)默認(rèn)的類加載委托規(guī)則,換言之,我們定制的類加載器的父加載器必須設(shè)置為null。
下面是一個(gè)很簡(jiǎn)單的官方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)行我們的程序,會(huì)輸出:
hello world! (version one)
hello world! (version one)
hello world! (version one)
現(xiàn)在對(duì)**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)
這里需要提及的是我們并未在測(cè)試類中使用了類型轉(zhuǎn)換(***Holder holder = (Holder)clazz.newInstance();***),這里就涉及到了我們?cè)谥疤岬降腏VM對(duì)類型的判定(由加載它的類和這個(gè)類本身一同確定其在JVM中的唯一性),如果進(jìn)行類型轉(zhuǎn)換那么會(huì)拋出*ClassCastException*異常。這是由于*clazz*是由我們自定義的類加載器的,而*holder*變量類型和轉(zhuǎn)型的*Holder*是由run方法所屬的類加載器(系統(tǒng)加載器)進(jìn)行加載的,所以會(huì)拋出異常。如果采用增加接口的方式進(jìn)行轉(zhuǎn)換,那么也是不可以的,原因也大致相同。
擴(kuò)展
在運(yùn)行時(shí)判斷系統(tǒng)類加載器加載路徑
一是可以直接調(diào)用*ClassLoader.getSystemClassLoader()*或者其他方式獲取到系統(tǒng)類加載器(系統(tǒng)類加載器和擴(kuò)展類加載器本身都派生自*URLClassLoader*),調(diào)用*URLClassLoader*中的*getURLs()*方法可以獲取到。
二是可以直接通過(guò)獲取系統(tǒng)屬性java.class.path來(lái)查看當(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
通過(guò)類加載器加載非類資源
ClassLoader除了用于加載類外,還可以用于加載圖片、視頻等非類資源。同樣可以采用雙親委派模型將加載資源的請(qǐng)求傳遞到頂層的引導(dǎo)類加載器,若失敗再逐層返回。
URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)
源碼上的一些小東西
對(duì)于*ClassLoader*的源碼也簡(jiǎn)單看了下,不過(guò)比較悲傷的是很多東西都看不懂,就把我能看懂的拿出來(lái)簡(jiǎn)單說(shuō)下,等以后能看懂了再來(lái)填這個(gè)坑。
前文中提到了**loadClass**方法是線程安全的,該方法是通過(guò)對(duì)**getClassLoadingLock**方法返回的Object完成的,我們就先來(lái)看看這個(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í)例,如果不為空,那么則通過(guò)**putIfAbsent(className, newLock);**方法來(lái)獲得一個(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;
我們?cè)谵D(zhuǎn)到**ClassLoader**的構(gòu)造函數(shù),這里有**parallelLockMap**變量初始化的過(guò)程:
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()**來(lái)給**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中通過(guò)這個(gè)類來(lái)指定并行能力,如果當(dāng)前的加載器具有并行能力,那么在根據(jù)類的名稱返回一個(gè)Object作為鎖,如果不具有并行能力,那就不用去創(chuàng)建這些東西了,直接把該實(shí)例鎖了就可以了,就醬。
結(jié)語(yǔ)
這篇文章由于我本身對(duì)類加載機(jī)制也不是分的了解,肯定還有很多的不足,也留了一些坑等著以后填,感覺(jué)要學(xué)的東西好多的說(shuō)。