《深刻理解Java虛擬機》虛擬機類加載機制

 

上節學習回顧java

 

上一節,咱們深刻到類文件去了解其結構細節,也大概對類文件的編寫規則略知一二了,解析來咱們就得學習這個類文件是如何被加載到Java虛擬機的,看看有什麼引人入勝的奧祕。程序員

 

本節學習重點數據庫

 

大部分計算機類科生都應該有接觸過C語言,C語言的編譯過程會有預處理、編譯、彙編、連接四個步驟,通過了這四個步驟就生成了能夠執行文件(二進制機器碼)。Java一樣如此,設計模式

有Java使用經驗的夥伴都應該知道,類加載有加載、鏈接、初始化三個階段,這就是虛擬機類加載的三大步驟,只有通過了這三大步驟才能在Java虛擬機上跑。惟一不一樣的是類文件是在程序運行期間執行的,而咱們本章學習的重點就是深刻這三大步驟,去發掘每一步驟的詳細操做和原理。數組

 

概述緩存

 

在Java語言裏面,類的加載、鏈接、初始化過程都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會爲Java應用程序提供高度的靈活性,Java裏天生能夠動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特色實現的tomcat

 

類加載時機安全

 

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期以下圖所示:網絡

如上圖,加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。這一點會在「解析」階段深刻學習並說明。數據結構

 

在Java虛擬機規範中並無進行強制約束,至於何時開始實行類加載的「加載」階段,這都就由虛擬機的具體實現來決定,但虛擬機規範嚴格規定了有且只有5種狀況必須對類進行「初始化」,固然初始化前的三個階段(加載、驗證、準備)就必須在此以前開始執行了。關於這5種必須初始化的場景以下:

1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有初始化,則須要先觸發其初始化;這4條指令對應的的常見場景分別是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

注:靜態內容是跟類關聯的而不是類的對象。

2)使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。

注:反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制,這相對好理解爲何須要初始化類。

3)當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。

注:子類執行構造函數前需先執行父類構造函數。

4)當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

注:main方法是程序的執行入口

5)當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化。則須要先觸發其初始化。

注:JDK1.7的一種新增的反射機制,都是對類的一種動態操做。

 

在虛擬機規範中使用了一個很強烈的限定語:「有且僅有」,這5種場景中的行爲稱爲對類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。看看如下三個例子:

演示一:經過子類引用父類的靜態字段,不會致使子類初始化

經過運行以上代碼能夠看到,此測試代碼只會輸出「SuperClass init!」而不會輸出「SubClass init!」。對於靜態字段,只有直接定義這個字段的類纔會被初始化。

 

演示二:經過數組定義來引用類,不會觸發此類的初始化

經過運行以上代碼,發現沒有任何輸出,說明並無觸發SuperClass的初始化。可是這段代碼觸發了另外一個名爲「[SuperClass」的類初始化階段,對於用戶代碼來講,這並非一個合法的類名稱,它是一個由虛擬機自動生成的、直接繼承於java.lang.Object的子類,建立動做由字節碼指令newarray觸發。這個類表明了一個元素類型爲SuperClass的一維數組,數組中應該有的屬性和方法(如length屬性和clone()方法)都實如今這個類裏。

 

演示三:常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化

運行以上代碼後並無輸出「SuperClass init!」,這是由於雖然在Java源碼中引用了SuperClass類中的常量value,但其實在編譯階段經過常量傳播優化,已經將此常量的值「hello world!」存儲到Test類的常量池中,之後Test類對常量SuperClass.value的引用實際上都轉化爲Test類對自身常量池的引用了。也就是說,實際上Test的class文件之中並無SuperClass類的符號引用入口,這兩個類在編譯成Class以後就不存在任何聯繫了。

 

類加載的過程:加載

 

「加載」和「類加載」是不一樣概念的,「加載」是「類加載」過程的一個階段,最好不要被字眼迷惑。在加載階段,虛擬機須要完成如下3件事情:

「加載」和「類加載」是不一樣概念的,「加載」是「類加載」過程的一個階段,最好不要被字眼迷惑。在加載階段,虛擬機須要完成如下3件事情:

1)經過一個類的全限定名來獲取此類的二進制字節流;

注:虛擬機規範並無明確說明類的二進制字節流從何而來,因此這裏能夠有很是靈活的實現空間,例如能夠用過ZIP包(如JAR、EAR、WAR格式)讀取,從網絡中獲取,運行時計算生成(如ASM框架),從數據庫中讀取等等。例如我經常使用的一個Websphere中間件跟tomcat中間件的類加載器就有所不一樣

2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;

注:回顧本書「2.2.5方法區」章節介紹:「方法區域Java堆同樣,是各線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據」。而方法區中的數據存儲結構格式虛擬機自行定義。

3)在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

注:加載階段完成後,虛擬機在內存中實例化一個java.lang.Class類的對象(Class是一個實實在在的對象,是記錄着類成員、接口等信息的對象)。還有一點是,咱們都知道對象確定是存放在堆中的,但Class對象比較特殊,對於HotSpot虛擬機而言,Class對象是存放在方法區中的。

 

非數組類和數據類的加載階段有有所不一樣,從以上「被動引用例子3」咱們就知道,數組類的應用是不會對該類進行初始化,而是虛擬機經過字節碼指令「newarray」去建立一個「[Object」對象。「初始化階段」是在「加載階段」以後,但不表明該類不會被加載。接下來,看看數組類加載過程要遵循的規則:

1)若是數組的組件類型是引用類型(非基礎類型),那就遞歸去加載這個組件類型(本章後續學習筆記會學習到類與類加載器的相關知識)。

2)若是數組組件類型不是引用類型(例如int[]數組),Java虛擬機將會把該數組標記爲與引導類加載器關聯。

3)數組類的可見性與他的組件類型可見性一致,若是組件類型不是引用類型,那數組的可見性將默認爲public

加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序,也就是必須先加載才能驗證。

 

類加載的過程:驗證

 

加載階段能夠說是字節碼進入Java虛擬機的入口,換個角度去想,若是咱們的API接受外來請求數據時,我應該要作些什麼樣的事情,那固然是對入參數據的驗證了。沒作,類加載過程的「驗證階段」一樣是對類文件的字節碼進行驗證,才能確保Java虛擬機不受惡意代碼的攻擊。從性能上講,這無疑是給虛擬機帶來額外的性能消耗,但這也是無可厚非要付出的代價。一開始《Java虛擬機規範(第2版)》對這個階段的限制、指導仍是比較籠統而粗糙的,直到《Java虛擬機規範(Java SE 7版)》才大幅細化了驗證過程的篇幅。從總體上看,驗證階段大體上會完成下面4個階段的檢查動做:

1)驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。細節以下:

l  是否以魔數0xCAFEBABE開頭;

l  主、次版本號是否在當前虛擬機處理範圍內;

l  常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌);

l  指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量;

l  CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據

l  Class文件中各個部分及文件是否有被刪除的或附加的其餘信息;

l  ……

2)元數據驗證,保證不存在不符合Java語言規範的元數據信息

l  這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類);

l  這個類的父類是否繼承了不容許被繼承的類(被final修飾的類);

l  若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法;

l  類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)。

l  ……

3)字節碼驗證,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件

l  保證任意時刻操做棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧放置了一個int類型的數據,使用時卻按long類型來加載如本地變量中;

l  保證跳轉指令不會跳轉到方法體之外的字節碼指令上;

l  保證方法體中的類型轉換是有效的;

l  ……

「字節碼驗證」是整個驗證階段最消耗時間的,雖然如此但也不能保證絕對安全。

4)符號引用驗證,確保在後續的「解析」階段能正常執行

l  符號引用中經過字符串描述的全限定名是否能找到對應的類;

l  在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段;

l  在符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問;

l  ……

其實咱們的IDE也虛擬機規範的檢查,因此咱們的代碼加載幾乎沒有不經過的。

 

類加載的過程:準備

 

準備階段是正式爲類變量分配內存設置類變量初始化值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念須要強調一下。首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:

public static int value = 123

那變量value在準備階段事後的初始化值爲0而不是123,由於這是還沒有開始執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後存放在類構造器<clinit>()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。如下表格列出了全部基本數據類型的零值:

數據類型

零值

int

0

long

0L

short

(short)0

char

‘\u0000’

byte

(byte)0

boolean

false

float

0.0f

double

0.0d

reference

null

上面提到的在「一般狀況」下初始值爲零值,但仍是會有一些特殊狀況,以下:

public static final int value = 123

類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化微ConstantValue屬性所指定的值。編譯時Javac將會爲vaue生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。

 

類加載的過程:解析

 

解析階段是虛擬機將常量池的符號引用直接替換爲直接引用的過程,看看前一章節的常量池例子:

看看截圖紅色框框的就是常量池的符號引用,再來解釋一下常量引用和符號引用的區別:

 

符號引用(Symbolic References):

符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須都是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。

 

直接引用(Direct References):

直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接點位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經在內存中存在。

 

就拿以上截圖的紅色框框的例子來舉例吧,框住的常量池語意大概是常量池中的第三個常量爲類或接口的符號引用,這個符號的值爲第四個常量池的值,也就是「java/lang/Object;」這是咱們熟知的Object類的全限定名。解析階段就是要把這個「class」的字符引用換成直接指向這個Object類在內存中的地址(如指針 )。那就說明,這個Object類必須同時也須要加載到內存中來。

 

對同一個符號引用進行屢次解析請求是很常見的事情,虛擬機實現能夠對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態)從而避免解析動做重複進行。但對於invokedynamic指令,上面規則則不成立。當碰到某個前面已經由invokedynamic指令觸發過解析的符號引用時,並不意味着這個解析結果對其餘invokedynamic指令也一樣生效。由於invokedynamic指令是JDK1.7新加入的指令,目的用於動態語言支持,它所對應的引用稱爲「動態調用點限定符」(Dynamic Call Site Specifier),這裏「動態」的含義就是必須等到程序實際運行到這條指令的時候,解析動做才能進行。相對的,其他可觸發解析的指令都是「靜態」的,能夠在剛剛完成加載階段,尚未執行代碼時就進行解析。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行引用,下面只對前4種引用的解析過程進行介紹,對於後面3種與JDK1.7新增的動態語言支持息息相關,後續章節將會學習到:

 

類或接口的解析:

 

假設當前代碼所處的類爲D,若是要把一個從未解析過的符號引用M解析爲 一個類或接口C的直接引用,那虛擬機完成整個解析過程須要一個3個步驟:

1)若是C不是一個數組類型,那虛擬機將會把表明N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程當中,因爲元數據驗證、字節碼驗證的須要,又可能觸發其餘相關類的加載動做,例如加載這個類的父類或實現接口。一旦這個加載過程出現了任何異常,解析過程宣佈失敗。

2)若是C是一個數組類型,而且數組的元素類型是對象,也就是N的描述符會是相似「[Ljava/lang/Integer」的形式,那將會按照第1點的規則加載數組元素類型。若是N的描述如前面所假設的形式,須要加載的元素類型就是「Java.lang.Integer」,接着有虛擬機生成一個表明此數組維度和元素的數組對象:「[Ljava/lang/Integer」(數組引用可回顧上文「類加載時機-被動引用演示二」)。

3)若是上述步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認D是否具有對C的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError異常。

 

字段解析:

 

要解析一個未被解析過的字段符號引用,首先將會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用。看看如下例子可能會更明白:

 

 

 

 

咱們對javap工具打印出Test.class的常量池看看:

行解析(也就是上圖的#14),那首先對t2字段所屬的Class進行解析,也就是#15的Test2。若是咱們在解析這個Test2類都失敗的話,那麼對Test的字段t2解析一樣失敗。若是解析Test2成功了那麼以上截圖紅色框框部分就是Test對Test2.t2字段的符號引用。若是咱們要對Test2.t2字段進咱們將這個這段所屬的類或接口用C(也就是以上例子的Test2)表示。虛擬機規範要求按照以下步驟對C進行後續字段的搜索:

1)若是C自己就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。

2)不然,若是在C中實現了接口,將會安裝繼承關係從上往下遞歸搜索各個接口和它的父接口,若是接口中包含了簡單名稱和字段描述符都與目標匹配的字段,則返回這個字段的直接引用,查找結束。

3)不然,若是C不是java.lang.Object的話,將會按照繼承關係從上往下遞歸搜索其父類,若是在父類中包含了簡單名稱和字段描述都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。

4)不然,查找失敗,拋出java.lang.NoSuchFieldError異常。

 

類方法解析:

 

類方法解析的第一個步驟與字段解析同樣,也須要解析出類方法表的class_index項中索引的方法所屬的類或接口的符號引用,若是解析成功咱們依然用C標識這個類,接下來虛擬機將會按照以下步驟進行後續的類方法搜索:

1)類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中發現class_index中索引的C是個接口,那直接拋出java.lang.IncompatibleClassChangeError異常。

2)若是經過了第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。

3)不然,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。

4)不然,在類C實現的接口列表及它們的父類接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是存在匹配方法,說明類C是一個抽象類,這是查找結束,拋出java.lang.AbstractMethodError異常。

5)不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

 

接口方法解析:

 

接口方法也須要先解析出接口方法表的class_index項中索引的方法屬性的類或接口的符號引用,若是解析成功,依然用C表示這個接口,接下來虛擬機將會按照以下步驟進行後續的接口方法搜索。

1)與類方法解析不一樣,若是在接口方法表中發現class_index中的索引C是個類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。

2)不然,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。

3)不然,在接口C的父接口中遞歸查找,知道java.lang.Object類(查找範圍會包Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。

4)不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

 

類加載的過程:初始化

 

類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器以外,其他動做徹底是由虛擬機主導和控制。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃其他初始化變量和其餘資源,或者從另外一個角度來表達,初始化階段是執行類構造器<clinit>()方法的過程:

■<clinit>()方法是有編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是有語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,但不能訪問,以下例子所示:

變量聲明前賦值例子

變量聲明前訪問例子

■<clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢;

 

■因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做;

 

■<clinit>()方法對於類和接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法;

 

■接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

 

■虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個進程阻塞。

 

注:我經過javap工具把class文件反編譯但找不到<clinit>()方法,只看到<init>()方法,我暫且偷懶經過網上資料獲得比較靠譜的答案是「由於這個特殊初始化方法是不能被Java代碼調用的,沒有任何一條invoke-*字節碼能夠調用它。它只能做爲類加載過程的一部分由JVM直接調用」,具體實現能夠參考JVM源碼。

 

類加載器

 

提到Java虛擬機加載器,確定會聯想到它的雙親委派機制,具體以下圖所示(由於懶得畫因此就網上借了個圖):

雙親委派機制圖

先來大概的解釋一下各個加載器的狀況:

 

■啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將<JAVA_HOME>\lib目錄中的,或被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(如rt.jar)類庫加載到虛擬機內存中。

Bootstrap ClassLoader是JVM系統級別的類加載器,應用是沒法使用的,例如Object類是由這個類加載器加載的,咱們嘗試去打印Object類的類加載器,獲得結果以下:

這就是JVM爲了保護Bootstrap ClassLoader所作的限制。

 

■擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現的,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器,以下示例:

圖(1)是個人ClassLoaderTest類在classpath下打印的結果:

圖(1)

圖(2)是個人ClassLoaderTest類打包放在<JAVA_HOME>/lib/ext目錄下的打印結果:

圖(2)

從以上實驗能夠看出,Extension ClassLoader確實能夠被用戶利用。

 

■應用程序類加載器(Application ClassLoader):從上面的測試能夠看到,這個類加載器由sun.misc.Launcher$AppClassLoader實現,因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

 

■自定義類加載器(User ClassLoader):全部自定義的類加載器必須繼承ClassLoader抽象類(嚴格說全部類加載器都繼承於它,除了Bootstrap ClassLoader,因它是由C/C++實現的),那先來看看ClassLoader有哪些重點方法:

方法

說明

getParent()

返回該類加載器的父類加載器。

loadClass(String name)

加載名稱爲 name的類,返回的結果是 java.lang.Class類的實例。

findClass(String name)

查找名稱爲 name的類,返回的結果是 java.lang.Class類的實例。

findLoadedClass(String name)

查找名稱爲 name的已經被加載過的類,返回的結果是 java.lang.Class類的實例。

defineClass(String name, byte[] b, int off, int len)

把字節數組 b中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的實例。這個方法被聲明爲 final的。

resolveClass(Class<?> c)

連接指定的 Java 類,調用的是本地方法。

除了以上ClassLoader抽象類的一些主要方法介紹,在本身寫自定義類加載器前仍是很是有必要講解一下類加載器的「雙親委派機制」。就如以上的「雙親委派機制圖」所示,它的工做過程是這樣的:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載器請求最終都是應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所須要的類)時,子加載器纔會嘗試本身去加載。如ClassLoader類的loadClass方法所示:

以上ClassLoader的loadClass方法的實現就是「雙親委派機制」的原型。除了「雙親委派機制」外,咱們還須要知道一點的是:對於任意一個類,都須要由它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性也就是說,同一個class文件,由不一樣的加載器去加載,都不相等。因此「雙親委派機制」有一個顯而易見的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,例如java.lang.Object,它存放於rt.jar之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類去加載,所以Object類在程序的各類類加載器環境中都是同一個類。除此以外,這種設計模式的其它優缺點須要各自腦補了。固然,這種設計默認並非必須的(後面會提到)。學習以上的知識,那麼就能夠自定義動手寫一個屬於本身的類加載器了,以下所示:

 1 public class MyClassLoader extends ClassLoader{  2     
 3     public MyClassLoader(){}  4       
 5     public MyClassLoader(ClassLoader parent){  6         super(parent);  7  }  8     
 9  @Override 10     protected Class<?> findClass(String name) throws ClassNotFoundException{ 11         
12         byte[] bytes = null; 13         
14         /*獲取類字節,自定義類我默認在G盤*/
15         FileInputStream fis = null; 16         try { 17             fis = new FileInputStream("G:\\"+name+".class"); 18             ByteArrayOutputStream baos = new ByteArrayOutputStream(); 19             byte[] buff = new byte[100]; 20             int rc = 0; 21             while ((rc = fis.read(buff, 0, 100)) > 0) { 22                 baos.write(buff, 0, rc); 23  } 24             bytes = baos.toByteArray(); 25             
26         } catch (Exception e) { 27  e.printStackTrace(); 28             
29         }finally{ 30             if(fis != null){ 31                 try { 32  fis.close(); 33                 } catch (IOException e) { 34  e.printStackTrace(); 35  } 36  } 37  } 38         
39         /*生產Class對象*/
40         try{ 41             Class<?> c = this.defineClass(name, bytes, 0, bytes.length); 42             return c; 43         }catch (Exception e){ 44  e.printStackTrace(); 45  } 46         
47         return super.findClass(name); 48  } 49     
50     public static void main(String[] args) throws Exception{ 51         MyClassLoader mlc = new MyClassLoader(MyClassLoader.getSystemClassLoader()); 52         Class c = mlc.loadClass("MyClassLoaderTest"); 53  System.out.println(c.newInstance().toString()); 54  } 55 
56 }

自定義一個類加載器的緣由有不少,例如應用須要加載不在ClassPath路徑下的類(重寫findClass方法),又或者不一樣插件容器須要不一樣加載器加載同一個類文件(重寫loadClass方法)等等。就像以上例子,我自定義了一個MyClassLoader重寫了findClass方法專門去加載我本地G盤的類。其實,只要獲得類文件的二進制流(甚至能夠經過ASM字節碼操做框架動態生成class二進制),就能夠初始化類對象,因此不管本地仍是遠程,均可以經過實現類加載。

 

總結

 

類加載確實是Java虛擬機的一大亮點,在本章也學習了類加載器委託、可見性以及單一性原理特性,許多人可能仍是會把類加載器跟「雙親委派機制」緊關聯甚至畫上等號,「雙親委派機制」是一種設計模式(代理模式),這種模式帶來的好處顯而易見,可是不一樣場景可能會有不一樣的場景需求而去破壞這種設計模式,例如許多WEB容器都有本身的類加載器,如Tomcat,它的自定義加載器首先會嘗試本身加載應用的類文件再交給父類加載器嘗試加載,這一點已經打破了雙親委派模型,但有些WEB容器又有本身的自定義規則,例如Websphere,全部本章重點在於理解類加載器原理,才能更好的掌控「格局」。

相關文章
相關標籤/搜索