一、為什么要使用類加載器?
二、類加載的過程 使用java編譯器可以把java代碼編譯為存儲(chǔ)字節(jié)碼的Class文件,使用其他語(yǔ)言的編譯器一樣可以把程序代碼翻譯成Class文件,java虛擬機(jī)不關(guān)心Class的來源是何種語(yǔ)言。如圖所示: 在Class文件中描述的各種信息,最終都需要加載到虛擬機(jī)中才能運(yùn)行和使用。那么虛擬機(jī)是如何加載這些Class文件的呢?
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的生命周期包括了:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)七個(gè)階段,其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱鏈接。 加載(裝載)、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段順序是固定的,類的加載過程必須按照這種順序開始,而解析階段不一定;它在某些情況下可以在初始化之后再開始,這是為了運(yùn)行時(shí)動(dòng)態(tài)綁定特性(JIT例如接口只在調(diào)用的時(shí)候才知道具體實(shí)現(xiàn)的是哪個(gè)子類)。值得注意的是:這些階段通常都是互相交叉的混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用或激活另外一個(gè)階段。
1.加載:(重點(diǎn)) 2.將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu) 3.在java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問入口 相對(duì)于類加載過程的其他階段,加載階段(準(zhǔn)備地說,是加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是開發(fā)期可控性最強(qiáng)的階段,因?yàn)榧虞d階段可以使用系統(tǒng)提供的類加載器(ClassLoader)來完成,也可以由用戶自定義的類加載器完成,開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式。 加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式有虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)并未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在java堆中實(shí)例化一個(gè)java.lang.Class類的對(duì)象,這個(gè)對(duì)象作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
2.驗(yàn)證:(了解) 驗(yàn)證是鏈接階段的第一步,這一步主要的目的是確保class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身安全。 1.文件格式驗(yàn)證 驗(yàn)證class文件格式規(guī)范,例如: class文件是否已魔術(shù)0xCAFEBABE開頭 , 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)等 2.元數(shù)據(jù)驗(yàn)證 這個(gè)階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證起描述的信息符合java語(yǔ)言規(guī)范要求。驗(yàn)證點(diǎn)可能包括:這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)、這個(gè)類是否繼承了不允許被繼承的類(被final修飾的)、如果這個(gè)類的父類是抽象類,是否實(shí)現(xiàn)了起父類或接口中要求實(shí)現(xiàn)的所有方法。 3.字節(jié)碼驗(yàn)證 進(jìn)行數(shù)據(jù)流和控制流分析,這個(gè)階段對(duì)類的方法體進(jìn)行校驗(yàn)分析,這個(gè)階段的任務(wù)是保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。如:保證訪法體中的類型轉(zhuǎn)換有效,例如可以把一個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,這是安全的,但不能把一個(gè)父類對(duì)象賦值給子類數(shù)據(jù)類型、保證跳轉(zhuǎn)命令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼命令上。 4.符號(hào)引用驗(yàn)證 符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類、符號(hào)引用類中的類,字段和方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問。 3.準(zhǔn)備:(了解) 準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的知識(shí)點(diǎn),首先是這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static 修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在java堆中。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量定義為: public static int value = 12; 那么變量value在準(zhǔn)備階段過后的初始值為0而不是12,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器 上面所說的“通常情況”下初始值是零值,那相對(duì)于一些特殊的情況,如果類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值,建設(shè)上面類變量value定義為: public static final int value = 123; 編譯時(shí)javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value設(shè)置為123。 4.解析:(了解) 直接引用:直接引用可以是直接指向目標(biāo)對(duì)象的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)內(nèi)存布局實(shí)現(xiàn)相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同,如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。 虛擬機(jī)規(guī)范并沒有規(guī)定解析階段發(fā)生的具體時(shí)間,只要求了在執(zhí)行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic這13個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們使用的符號(hào)引用進(jìn)行解析,所以虛擬機(jī)實(shí)現(xiàn)會(huì)根據(jù)需要來判斷,到底是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析,還是等到一個(gè)符號(hào)引用將要被使用前才去解析它。 解析的動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法四類符號(hào)引用進(jìn)行。分別對(duì)應(yīng)編譯后常量池內(nèi)的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四種常量類型。 1.類、接口的解析 2.字段解析 3.類方法解析 4.接口方法解析 5.初始化:(了解) 類的初始化階段是類加載過程的最后一步,在準(zhǔn)備階段,類變量已賦過一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器 1.遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需先觸發(fā)其初始化。生成這4條指令的最常見的java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用類的靜態(tài)方法的時(shí)候。 2.使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候 3.當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化、則需要先出發(fā)其父類的初始化 4.jvm啟動(dòng)時(shí),用戶指定一個(gè)執(zhí)行的主類(包含main方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)類 在上面準(zhǔn)備階段 public static int value = 12; 在準(zhǔn)備階段完成后 value的值為0,而在初始化階調(diào)用了類構(gòu)造器 *類構(gòu)造器 *類構(gòu)造器 *由于父類的 * *接口中不能使用靜態(tài)語(yǔ)句塊,但接口與類不太能夠的是,執(zhí)行接口的 *虛擬機(jī)會(huì)保證一個(gè)類的 三、類加載器 JVM設(shè)計(jì)者把類加載階段中的“通過'類全名'來獲取定義此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放到Java虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”。 1.類與類加載器 對(duì)于任何一個(gè)類,都需要由加載它的類加載器和這個(gè)類來確立其在JVM中的唯一性。也就是說,兩個(gè)類來源于同一個(gè)Class文件,并且被同一個(gè)類加載器加載,這兩個(gè)類才相等。 2.雙親委派模型 從虛擬機(jī)的角度來說,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器(Bootstrap ClassLoader),該類加載器使用C++語(yǔ)言實(shí)現(xiàn),屬于虛擬機(jī)自身的一部分。另外一種就是所有其它的類加載器,這些類加載器是由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立于JVM外部,并且全部繼承自抽象類java.lang.ClassLoader。 從Java開發(fā)人員的角度來看,大部分Java程序一般會(huì)使用到以下三種系統(tǒng)提供的類加載器: 我們的應(yīng)用程序都是由這三類加載器互相配合進(jìn)行加載的,我們也可以加入自己定義的類加載器。這些類加載器之間的關(guān)系如下圖所示: 如上圖所示的類加載器之間的這種層次關(guān)系,就稱為類加載器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。子類加載器和父類加載器不是以繼承(Inheritance)的關(guān)系來實(shí)現(xiàn),而是通過組合(Composition)關(guān)系來復(fù)用父加載器的代碼。 通過上面代碼可以看出,雙親委派模型是通過loadClass()方法來實(shí)現(xiàn)的,根據(jù)代碼以及代碼中的注釋可以很清楚地了解整個(gè)過程其實(shí)非常簡(jiǎn)單:先檢查是否已經(jīng)被加載過,如果沒有則調(diào)用父加載器的loadClass()方法,如果父加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器。如果父類加載器加載失敗,則先拋出ClassNotFoundException,然后再調(diào)用自己的findClass()方法進(jìn)行加載。 3.自定義類加載器 若要實(shí)現(xiàn)自定義類加載器,只需要繼承java.lang.ClassLoader 類,并且重寫其findClass()方法即可。java.lang.ClassLoader 類的基本職責(zé)就是根據(jù)一個(gè)指定的類的名稱,找到或者生成其對(duì)應(yīng)的字節(jié)代碼,然后從這些字節(jié)代碼中定義出一個(gè) Java 類,即 java.lang.Class 類的一個(gè)實(shí)例。除此之外,ClassLoader 還負(fù)責(zé)加載 Java 應(yīng)用所需的資源,如圖像文件和配置文件等,ClassLoader 中與加載類相關(guān)的方法如下: loadClass(String name) 加載名稱為 二進(jìn)制名稱為name 的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 findClass(String name) 查找名稱為 name 的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 findLoadedClass(String name) 查找名稱為 name 的已經(jīng)被加載過的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 resolveClass(Class?> c) 鏈接指定的 Java 類。 在Java中,任意一個(gè)類都需要由加載它的類加載器和這個(gè)類本身一同確定其在java虛擬機(jī)中的唯一性,即比較兩個(gè)類是否相等,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提之下才有意義,否則,即使這兩個(gè)類來源于同一個(gè)Class類文件,只要加載它的類加載器不相同,那么這兩個(gè)類必定不相等(這里的相等包括代表類的Class對(duì)象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof關(guān)鍵字的結(jié)果)。例子代碼如下: 類加載器雙親委派模型是從JDK1.2以后引入的,并且只是一種推薦的模型,不是強(qiáng)制要求的,因此有一些沒有遵循雙親委派模型的特例:(了解) (1).在JDK1.2之前,自定義類加載器都要覆蓋loadClass方法去實(shí)現(xiàn)加載類的功能,JDK1.2引入雙親委派模型之后,loadClass方法用于委派父類加載器進(jìn)行類加載,只有父類加載器無法完成類加載請(qǐng)求時(shí)才調(diào)用自己的findClass方法進(jìn)行類加載,因此在JDK1.2之前的類加載的loadClass方法沒有遵循雙親委派模型,因此在JDK1.2之后,自定義類加載器不推薦覆蓋loadClass方法,而只需要覆蓋findClass方法即可。 (2).雙親委派模式很好地解決了各個(gè)類加載器的基礎(chǔ)類統(tǒng)一問題,越基礎(chǔ)的類由越上層的類加載器進(jìn)行加載,但是這個(gè)基礎(chǔ)類統(tǒng)一有一個(gè)不足,當(dāng)基礎(chǔ)類想要調(diào)用回下層的用戶代碼時(shí)無法委派子類加載器進(jìn)行類加載。為了解決這個(gè)問題JDK引入了ThreadContext線程上下文,通過線程上下文的setContextClassLoader方法可以設(shè)置線程上下文類加載器。 JavaEE只是一個(gè)規(guī)范,sun公司只給出了接口規(guī)范,具體的實(shí)現(xiàn)由各個(gè)廠商進(jìn)行實(shí)現(xiàn),因此JNDI,JDBC,JAXB等這些第三方的實(shí)現(xiàn)庫(kù)就可以被JDK的類庫(kù)所調(diào)用。線程上下文類加載器也沒有遵循雙親委派模型。 (3).近年來的熱碼替換,模塊熱部署等應(yīng)用要求不用重啟java虛擬機(jī)就可以實(shí)現(xiàn)代碼模塊的即插即用,催生了OSGi技術(shù),在OSGi中類加載器體系被發(fā)展為網(wǎng)狀結(jié)構(gòu)。OSGi也沒有完全遵循雙親委派模型。 4.動(dòng)態(tài)加載Jar && ClassLoader 隔離問題 動(dòng)態(tài)加載Jar: Java 中動(dòng)態(tài)加載 Jar 比較簡(jiǎn)單,如下: 表示加載 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,可以為當(dāng)前的 ClassLoader。 大家覺得一個(gè)運(yùn)行程序中有沒有可能同時(shí)存在兩個(gè)包名和類名完全一致的類? 當(dāng)碰到這種問題時(shí)可以通過 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一樣。 加載不同 Jar 包中公共類: 現(xiàn)在 Host 工程包含了 common.jar, jar1.jar, jar2.jar,并且 jar1.jar 和 jar2.jar 都包含了 common.jar,我們通過 ClassLoader 將 jar1, jar2 動(dòng)態(tài)加載進(jìn)來,這樣在 Host 中實(shí)際是存在三份 common.jar,如下圖: https://farm4./3872/14301963930_2f0f0fe8aa_o.png 我們?cè)趺幢WC common.jar 只有一份而不會(huì)造成上面3中提到的 ClassLoader 隔離的問題呢,其實(shí)很簡(jiǎn)單,在生成 jar1 和 jar2 時(shí)把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 為 parentClassLoader 即可。 最后: 一道面試題 能不能自己寫個(gè)類叫java.lang.System? 答案:通常不可以,但可以采取另類方法達(dá)到這個(gè)需求。 但是,我們可以自己定義一個(gè)類加載器來達(dá)到這個(gè)目的,為了避免雙親委托機(jī)制,這個(gè)類加載器也必須是特殊的。由于系統(tǒng)自帶的三個(gè)類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個(gè)特殊的目錄,那么系統(tǒng)的加載器就無法加載,也就是最終還是由我們自己的加載器加載。 本文系轉(zhuǎn)載,原文鏈接: http://blog.csdn.net/boyupeng/article/details/47951037 |
|