Java類加載
? ? ? ?在Java中,一個類如果要想正確運行,就必須通過JVM編譯幕垦,然后將其載入內存中才能使用,這里的載入內存中傅联,實際上就是:類在JVM中以java.lang.Class類型的對象存在薄风。說到Class類型瘫俊,了解反射的都比較清楚,這是反射中常用的一個類型,獲取Class對象一般使用兩種方式:通過類的具體對象的getClass方法(obj.getClass())划纽、通過類的class屬性獲取(Object.class)痊臭。
? ? ? ?Java中的類加載實際上都是通過字節(jié)流來來實現(xiàn)的,字節(jié)流的來源可以有多種方式,可以從文件中獲取岛抄,也可以通過網絡獲取,雖然來源不同狈茉,但是只要最終是字節(jié)流形式夫椭,就可以通過JVM來解析并加載。在此先說明一下:類在加載過程中都經歷了那些步驟氯庆。
類的生命周期
? ? ? ?類從被虛擬機加載蹭秋,直到最后被卸載出內存,這一整個過程成為類的生命周期点晴,大致可以分為七個階段:
? ? ? ?這里需要注意:除去解析這個步驟感凤,其余步驟的順序都是確定的,解析階段規(guī)則有點不同粒督,在有些情況下陪竿,解析可以在初始化之后再進行,這也是Java語言的運行時綁定的一個基礎保障屠橄。
? ? ? ?這里先說說初始化族跛,在虛擬機規(guī)范中,嚴格規(guī)定了初始化所必須具備的條件锐墙,即:如果滿足必備條件礁哄,將必須執(zhí)行初始化操作。共有5中情況(能夠到初始化這一步溪北,也就說明前面都已經驗證通過了):
遇到new桐绒、getstatic、putstatic或invokestatic這四條指令代碼之拨,如果對應類還沒有初始化茉继,則必須進行初始化,對應與Java中常用的場景就是:實例化一個對象(new)蚀乔、讀取或設置一個靜態(tài)字段(getstatic烁竭、putstatic;這里需要額外說明:final修飾的常量不包括在內吉挣,因為它屬于在編譯器就已經把它放到了靜態(tài)常量池中了)派撕、調用一個方法的時候(invokestatic)。
在使用reflect包下的方法時睬魂,對類進行反射調用的時候终吼,如果沒有初始化,先觸發(fā)其初始化
當初始化某一個類的時候氯哮,它的父類還沒有進行初始化衔峰,那么就先初始化它的父類
虛擬機啟動時,我們指定的那個包含main方法的類(執(zhí)行主類)會先被初始化
JDK1.7的動態(tài)語言支持部分,在使用java.lang.invoke.MethodHandler實例解析最后結果中的方法句柄垫卤,如果方法句柄所對應的類沒有被初始化,則需要先觸發(fā)其初始化出牧。
類加載
? ? ? ?類的加載過程可以分為加載穴肘、驗證、準備舔痕、解析和初始化這五個步驟评抚。也就是上圖中除去使用和卸載兩個步驟之外的過程。
加載
? ? ? ?加載過程是類加載過程的一個部分伯复,換句話說:ClassLoading過程包括加載過程慨代,但不僅僅只有加載過程,在加載階段啸如,虛擬機需要完成3個步驟:
通過一個類的全限定名來獲取此類的二進制字節(jié)流侍匙。
將此字節(jié)流所代表的靜態(tài)存儲結構轉換成方法區(qū)中的運行時數(shù)據(jù)結構
在內存中生成一個Class對象,在方法區(qū)中作為這個類訪問入口叮雳。
? ? ? ?通過第一步可以知道想暗,這里并沒有準確定義二進制流具體要從哪里獲取,怎樣獲取帘不。因此可以發(fā)揮的空間就比較大了说莫,例如:可以通過壓縮包中讀取(zip寞焙、jar储狭、war等等)、可以從網絡中獲取捣郊、運行時動態(tài)計算(動態(tài)代理)辽狈、其他文件生成(JSP)等等,方法多樣模她,可以根據(jù)具體應用場景來自由選擇稻艰。
驗證
? ? ? ?這一步是至關重要的一部,目的就是為了確保二進制字節(jié)流中包含的信息是不是與當前虛擬機的要求相符合侈净,同時會不會對虛擬機有危險尊勿。驗證階段的嚴謹性直接決定了虛擬機的強壯性,嚴謹?shù)尿炞C過程可以保證虛擬機能夠抵御各種惡意代碼的攻擊畜侦。這一過程如果不通過元扔,會拋出java.lang.VerifyError。主要分為四個步驟:
文件格式驗證:就是驗證字節(jié)流是否符合規(guī)范旋膳,如:是否以魔數(shù)0xCAFEBABE開頭澎语、主次版本號是否在虛擬機的處理范圍之內...等等
元數(shù)據(jù)驗證:這段主要是對字節(jié)流中的信息進行語義分析,保證符合Java的語言規(guī)范,如:驗證是否有父類(除Object之外擅羞,所有類都應當有父類)尸变、是否存在繼承了final修飾的類...等等
字節(jié)碼驗證:這塊比較復雜,目的就是通過數(shù)據(jù)流和控制流分析减俏,確定程序語義是合法的召烂、符合邏輯的,在元數(shù)據(jù)校驗的基礎上娃承,會對方法體進行校驗分析奏夫,保證方法在運行時不會對虛擬機有危害。
符號引用驗證:這個步驟發(fā)生在虛擬機將符號引用轉化為直接引用過程中历筝,這個轉化動作發(fā)生在上圖類的生命周期所示的【連接】步驟的第三個階段--【解析】階段發(fā)生酗昼。可以看做是類對自身以外的信息進行匹配校驗梳猪。
準備
? ? ? ?這個步驟是正式為類變量分配內存并且設置初始值的階段麻削,這些變量使用的內存都將在方法區(qū)中進行分配。注意這里說的變量僅僅是static修飾的變量舔示,也就是跟類相關的變量碟婆,實例變量是在類進行實例化的時候,在Java堆中進行分配的惕稻。另外這里說的初始化并不是我們程序中定義的那些初始化的值竖共,它僅僅只是根據(jù)數(shù)據(jù)類型設置該類型的零值。例如i程序中定義了變量:
public static int value = 123;
? ? ? ?那么這里在初始化的時候俺祠,value的值此時是0公给,而value賦值123的操作是在程序被編譯之后進行的,123的賦值操作將會在后面【初始化】那一步進行的蜘渣。下面給出一些基本數(shù)據(jù)類型對應的零值:
數(shù)據(jù)類型 | 零值 | 數(shù)據(jù)類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
? ? ? ?上面的零值是通常情況下的賦值淌铐,但是還有一些特殊情況,例如用final修飾的變量蔫缸,它所修飾的變量在編譯階段都會生成ConstantValue屬性腿准,并且將該屬性的值指向程序所設置的值,如上面例子中的value拾碌,如果加上final吐葱,ConstantValue就會指向123,因此在準備階段校翔,虛擬機就會根據(jù)該屬性指向的值設置對應字段的值弟跑。
解析
? ? ? ?這個階段是虛擬機將常量池中的符號引用轉換為直接引用的過程,那么符號引用和直接引用到底是什么概念防症?
符號引用(Symbolic Reference):它只是一種用來表示某種目標的一種標記孟辑,通過它能夠讓虛擬機定位到它所代指的目標即可哎甲,它可以是任何形式的字面量,它與虛擬機的內存布局無關饲嗽,只是一種概念上的存在炭玫,它所代指的目標不要求是否已經在內存中存在,不同虛擬機雖然內存布局不一樣貌虾,但是所接受的符號引用是一致的础嫡,所以說它是通用的。
直接引用(Direct Reference):它可以是一個能夠直接指向目標的指針酝惧、相對偏移量或者一個能間接訪問到目標的句柄。它與具體的內存布局相關伯诬,它所指向的目標都已經是存在與內存中晚唇,是真實存在的。
? ? ? ?解析主要包括:類或接口的解析盗似、字段解析哩陕、類方法解析、接口方法解析赫舒。這里不再分析具體各個解析的概念悍及,想要了解詳情,可以查閱《深入理解Java虛擬機 第2版》的第七章第三小節(jié)解析部分接癌。
初始化
? ? ? ?到了這一步心赶,才開始執(zhí)行類中定義的Java程序代碼,在前面已經有過一步初始化步驟缺猛,在這一步缨叫,將會根據(jù)Java程序的主觀意愿去初始化變量和其他資源。初始化階段是執(zhí)行類構造器<cinit>()方法的過程荔燎。這里有一種情況需要說明:Java在定義靜態(tài)語句塊的時候耻姥,靜態(tài)語句塊中只能訪問到定義在該靜態(tài)語句塊之前的變量,對于定義在它之后的變量有咨,可以賦值琐簇,但是不能訪問,例如:
public class Test {
static {
i = 0;//可以編譯通過座享,正常賦值
System.out.println(i);//編譯不通過婉商,提示“非法向前引用(Illegal forward reference)”
}
static int i = 1;
}
? ? ? ?而且<cinit>()方法執(zhí)行之前,父類的<cinit>()方法必須已經執(zhí)行完畢了征讲,所以說最先執(zhí)行的<cinit>()方法一定是Object類的据某,并且父類的靜態(tài)語句要先于子類的靜態(tài)語句執(zhí)行。
類加載器
? ? ? ?類加載器作用是實現(xiàn)類的加載動作诗箍,同時它也是區(qū)分類與類之間唯一性的重要依據(jù)癣籽。在Java中對于任意一個類挽唉,都需要由加載器和類的本身一同確定類在Java虛擬機中的唯一性。每一個類加載器都有一個獨立的命名空間筷狼。換句話說:如果要比較兩個類是不是“相等”瓶籽,首先得基于是同一個加載器加載出來的,否則兩個類肯定是“不相等”的埂材。即:同一份字節(jié)碼文件塑顺,被不同的加載器加載,那這兩個類仍然是“不相等”的俏险。
雙親委派模型
? ? ? ?在虛擬機中严拒,存在兩大類加載器:根加載器(BootStrap ClassLoader)和其他以Java實現(xiàn)的加載器。除根加載器之外竖独,其余的加載器都繼承自抽象類ClassLoader裤唠,并且由Java代碼實現(xiàn)。
根加載器:是C++實現(xiàn)的莹痢,它主要是用于加載JAVA_Home\lib目錄中的類种蘸,或者被-Xbootclasspath參數(shù)指定的路徑。這里虛擬機識別的方式是按照名字識別的竞膳,換句話說:它識別的那些類都是已經定義好的航瞭,如果是不符合這些命名的,即使放進加載路徑中坦辟,也不會加載刊侯。
擴展類加載器(Extension ClassLoader):主要是加載JAVA_HOME\lib\ext路徑下的類。
應用程序加載類(Application ClassLoader):加載類路徑上指定的類庫长窄,如果程序中沒有自定的加載器滔吠,這個就是默認的加載器,用戶編寫的代碼挠日,一般都是通過它來加載疮绷。
? ? ? ?上面的三類加載器,是分層級的嚣潜,最底層是應用程序加載器冬骚,上面一層是擴展類加載器,最頂層的是根加載器懂算。雙親委派模型就是依據(jù)此結構建立的只冻,它具體工作過程是:如果一個加載器收到了類加載的請求,加載器并不會直接進行加載计技,而是把這個請求委派個父類加載器來完成喜德,只有父類加載器無法完成的時候,才到子類加載器中加載垮媒。另外需要明確一點:這里雖然說是“父子”關系舍悯,但是實際上并不是繼承關系航棱,而是通過組合方式實現(xiàn)的。在獲取類加載器的時候萌衬,可以通過getParent方法來獲取對應加載器的父類加載器饮醇。Application ClassLoader的父類加載器是Extension ClassLoader;Extension ClassLoader的父類加載器是BootStrap ClassLoader秕豫。但是如果我們通過Extension ClassLoader的getParent方法獲取父類加載器的時候朴艰,得到的會是null,這是因為根加載器是C++實現(xiàn)的混移,它是本地語言實現(xiàn)的祠墅。
雙親委派模型有一個好處,就是Java類隨著加載器的不同歌径,有了優(yōu)先級劃分饵隙,同時對于Java程序的安全性和穩(wěn)定性有了保障。試想:如果沒有雙親委派沮脖,隨便一個類都可以指定類加載器進行加載,那如果用戶自定義了一個Object類芯急,指定根加載器加載勺届,這就破壞了Java內部的繼承結構,Java內部娶耍,所有的類都是直接或間接繼承自Object免姿,這樣出現(xiàn)了多個Object類,Java體系內部榕酒,一些很基礎的行為就無法保證了(例如:hash胚膊,toString,equals等等這些行為)想鹰。這樣系統(tǒng)內部就會一片混亂紊婉。而有了雙親委派,就能夠保證越基礎的類辑舷,由越高級的類加載器去完成喻犁,例如:用戶嘗試編寫一個與rt.jar類庫中某個類重名的類,可以發(fā)現(xiàn)何缓,雖然可以編譯肢础,但是永遠不會被加載運行,因為在到根加載器驗證的時候就無法通過碌廓。
雙親委派模型在Java的ClassLoader.java的源碼中就可以看到传轰,可以查看該類中的loadClass方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,驗證類是不是已經被加載過了
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
}
if (c == null) {
//此時仍然沒有找到慨蛙,就調用本身的findClass方法
long t1 = System.nanoTime();
c = findClass(name);
//下面這些步驟就是定義類加載器辽聊,并且記錄狀態(tài)的,暫時無視它
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看到股淡,代碼邏輯很清晰:
先根據(jù)findLoadedClass確定是不是已經加載過
如果沒有身隐,就委派父類加載器進行查找加載
如果父類加載器沒有加載成功,就拋出ClassNotFoundException唯灵,并且調用本加載器的findClass方法進行加載
破壞雙親委派模型
? ? ? ?需要明白的是雙親委派模型不是一個強制性的約束贾铝,它只是一種推薦模式,Java中大都遵循這種模型埠帕,但是也會有例外垢揩,目前為止,雙親委派模型經歷過三次較大規(guī)模的“被破壞”情況敛瓷。其實與其說是“被破壞”叁巨,我更愿意稱它為“被改造”。因為帶來這些“破壞”的根本原因呐籽,主要還是因為雙親委派在有些特殊應用場景無法滿足的問題锋勺。
? ? ? ?第一次被破壞是JDK1.2之前,因為雙親委派模型是在JDK1.2之后才發(fā)布的狡蝶,ClassLoader在JDK1.0就已經存在了庶橱,因此在1.2版本發(fā)布后,需要兼容以前的代碼贪惹,1.2以后的ClassLoader添加一個protected方法findClass苏章。這么做的原因就是在于1.2以前,用戶繼承ClassLoader的唯一目的就是重寫loadClass方法奏瞬,那么虛擬機在調用類加載器的時候會調用加載器的私有方法loadClassInternal方法枫绅,該方法就一個邏輯:調用自己寫的loadClass方法,在1.2以后硼端,不提倡覆蓋loadClass方法并淋,建議重寫findClass方法,這樣在父類的loadClass加載失敗之后珍昨,會直接調用自身實現(xiàn)的findClass方法來加載预伺。以此保證新寫出來的類加載器是符合雙親委派模型的。
? ? ? ?第二次被破壞是模型本身缺陷造成的曼尊,因為該模型雖然可以讓越基礎的類由越高級的加載器完成加載酬诀,但是也限制了上層加載器中加載的類不能調用用戶的代碼,典型的例子就是JNDI服務骆撇,它在服務啟動的時候瞒御,需要調用各個廠商實現(xiàn)的不同接口,而該服務本身是放在rt.jar中的神郊,為了解決這個問題肴裙,引入了線程上下文類加載器趾唱,它可以通過Thread類的setContextClassLoader方法來設置,若線程創(chuàng)建時沒有指定蜻懦,就直接從父類中繼承一個甜癞,如果程序的全局環(huán)境都沒有設置,就采用默認的應用程序類加載器宛乃,有了個這種加載器悠咱,JNDI通過該線程去加載所需要的SPI代碼,即:父類加載器請求子類加載器去完成類的加載征炼,實際上就是雙親委派的逆向過程析既。常說的JNDI、JDBC等采用的都是這種方式谆奥。
? ? ? ?第三次“被破壞”是用戶對程序動態(tài)性追求而導致的眼坏。這里的動態(tài)性實際就是指:代碼熱替換、模塊熱部署等這些“熱點”概念酸些。就是希望在程序運行的時候在不需要重啟應用程序的情況下宰译,動態(tài)替換程序中的類。這里就不得不提OSGi魄懂,它是Sun公司提出的JSR-294囤屹、JSR-277規(guī)范與JCP組織的模塊化規(guī)范斗爭中的產物,最終Sun落敗給JSR-294規(guī)范(即:OSGi R4.2)逢渔,OSGi實現(xiàn)熱部署的關鍵就是:它有自定義的類加載器機制實現(xiàn),每個模塊在OSGi中都是一個Bundle乡括,它都有一個自己的類加載器肃廓,當需要更換模塊的時候,就將該Bundle連同所帶的類加載器一起換掉诲泌,從而達到熱部署的效果盲赊。而且在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結構敷扫,而是進一步發(fā)展為網狀結構哀蘑。