类加载机制-双亲委派,破坏双亲委派--这一篇全了解
概述
概念
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接時候用的Java類型。
類的生命周期
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用、卸載。其中驗證、準(zhǔn)備、解析統(tǒng)稱為連接
上圖中,加載、驗證、準(zhǔn)備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須嚴(yán)格按照這種順序開始。
解析階段則不一定,它在某些情況下,可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(動態(tài)綁定|晚期綁定)
類加載-時機(jī)
主動引用
Java虛擬機(jī)規(guī)范中并沒有進(jìn)行強(qiáng)制約束什么時候開始類加載過程的第一個階段-加載,可以交給虛擬機(jī)具體實現(xiàn)來自由把握。但對于初始化階段,虛擬機(jī)規(guī)范嚴(yán)格規(guī)定有且只有5種情況必須立即對類進(jìn)行初始化(加載、驗證、準(zhǔn)備自然要在此之前開始)
遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)初始化操作。
4條指令最常見Java代碼場景:用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候、調(diào)用一個類的靜態(tài)方法的時候。
用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行過初始化,則需要觸發(fā)初始化操作。
初始化一個類的時候,發(fā)現(xiàn)其父類還有進(jìn)行過初始化,則需要觸發(fā)先其父類的初始化操作。
注意這里和接口的初始化有點區(qū)別,,一個接口在初始化時,并不要求其父接口全部都完成了初始化,只要在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。
虛擬機(jī)啟動時,需要指定一個執(zhí)行的主類(包含main方法的類),虛擬機(jī)會先初始化這類。
用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化操作。
被動引用
以上5種場景均有一個必須的限定:“有且只有”,這5種場景中的行為稱為對一個類進(jìn)行主動引用。除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用。
示例1
? ? package com.xdwang.demo;
? ? /**
? ? ?* 通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化
? ? ?*/
? ? public class SuperClass {
? ? ? ? static {
? ? ? ? ? ? System.out.println("SuperClass init....");
? ? ? ? }
?
? ? ? ? public static int value = 123;
? ? }
?
? ? package com.xdwang.demo;
?
? ? public class SubClass extends SuperClass {
? ? ? ? static {
? ? ? ? ? ? System.out.println("SubClass init....");
? ? ? ? }
? ? }
?
? ? package com.xdwang.demo;
?
? ? public class Test {
? ? ? ? public static void main(String[] args) {
? ? ? ? ? ? System.out.println(SubClass.value);
? ? ? ? }
? ? }
運行結(jié)果:
SuperClass init....
123
結(jié)論:
對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。(是否觸發(fā)子類的加載和驗證,取決于虛擬機(jī)具體的實現(xiàn),對于HotSpot來說,可以通過-XX:+TraceClassLoading參數(shù)觀察到此操作會導(dǎo)致子類的加載)
示例2
package com.xdwang.demo;
?
public class Test2 {
? ? public static void main(String[] args) {
? ? ? ? //
? ? ? ? SuperClass[] superClasses = new SubClass[10];
? ? }
}
無任何輸出
結(jié)論:
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
這里其實會觸發(fā)另一個類的初始化
示例3
? ? package com.xdwang.demo;
?
? ? public class ConstClass {
? ? ? ? static {
? ? ? ? ? ? System.out.println("ConstClass init....");
? ? ? ? }
?
? ? ? ? public static final String MM = "hello Franco";
? ? }
?
? ? package com.xdwang.demo;
?
? ? public class Test3 {
? ? ? ? public static void main(String[] args) {
? ? ? ? ? ? System.out.println(ConstClass.MM);
? ? ? ? }
? ? }
運行結(jié)果:
hello Franco
并沒有ConstClass init….,這是因為雖然Test3里引用了ConstClass類中的常量,但其實在編譯階段通過常量傳播優(yōu)化,已經(jīng)將此常量存儲到Test3類的常量池中。兩個類在編譯成class之后就不存在任何聯(lián)系了。
類加載-過程
加載
加載階段(可參考java.lang.ClassLoader的loadClass()方法),虛擬機(jī)要完成以下3件事情:
通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流(并沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網(wǎng)絡(luò)、動態(tài)生成、數(shù)據(jù)庫等);
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口;
加載階段和連接階段(Linking)的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動作,仍然屬于連接階段的內(nèi)容,這兩個階段的開始時間仍然保持著固定的先后順序。
驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。
驗證階段是非常重要的,這個階段是否嚴(yán)謹(jǐn),直接決定了Java虛擬機(jī)是否能承受惡意代碼的工具,從執(zhí)行性能的角度上講,驗證階段的工作量在虛擬機(jī)的類加載子系統(tǒng)中又占了相當(dāng)大一部分。
驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能夠被當(dāng)前版本的虛擬機(jī)處理
是否以魔術(shù)0xCAFEBABE開頭
主次版本號是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)
常量池中的常量是否有不被支持的類型。
….
元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進(jìn)行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規(guī)范的要求;
這個類是否有父類。(除了java.lang.Object之外)
這個類的父類是否集繼承了不允許被繼承的類(被final修飾的類)
如果這個類不是抽象類,是否實現(xiàn)了其父類或接口中要求實現(xiàn)的所有方法
….
字節(jié)碼驗證:整個驗證過程最復(fù)雜的一個階段。主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段將對類的方法體進(jìn)行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機(jī)安全的事件
保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會出現(xiàn)類似在操作棧放int型數(shù)據(jù),使用卻按long行加載如本地變量表中。
保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體意外的字節(jié)碼指令上
….
符號引用驗證:目的是確保解析動作能正常執(zhí)行,發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)換為直接引用的時候,這個轉(zhuǎn)化動作將在連接的第三階段-解析階段中發(fā)生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗。
符號引用中通過字符串描述的全限定名是否能夠找到對應(yīng)的類。
在指定類中是否存在符號方法的字段描述符以及簡單名稱所描述的方法和字段
符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問。
….
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機(jī)類加載的時間。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這時候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在堆中。其次,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個類變量的定義為:
public static int value=123;
那變量value在準(zhǔn)備階段過后的初始值為0而不是123.因為這時候尚未開始執(zhí)行任何java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執(zhí)行。
至于“特殊情況”是指:
public static final int value=123
即當(dāng)類字段的字段屬性是ConstantValue時,會在準(zhǔn)備階段初始化為指定的值,所以標(biāo)注為final之后,value的值在準(zhǔn)備階段初始化為123而非0.
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進(jìn)行。
初始化
類初始化階段是類加載過程的最后一步,到了初始化階段,才真正開始執(zhí)行類中定義的java程序代碼。在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序猿通過程序制定的主觀計劃去初始化類變量和其他資源,或者說:初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊static{}中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問。如下:
public class Test
{
? ? static
? ? {
? ? ? ? i=0;//給變量賦值可以正常編譯通過
? ? ? ? System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應(yīng)用)
? ? }
? ? static int i=1;
}
?
<clinit>()方法與實例構(gòu)造器<init>()方法不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機(jī)會保證在子類<init>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢,一次虛擬機(jī)中第一個被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object。
由于父類的<clinit>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。(下面的例子,B=2)
static class Parent{
? ? public static int A=1;
? ? static{
? ? ? ? A=2;
? ? }
}
static class Sub extends Parent{
? ? public static int B=A;
}
public class Test{
? ? public static void main(String[] args){
? ? ? ? System.out.println(Sub.B);
? ? }
}
<clinit>()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生產(chǎn)<clinit>()方法。
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當(dāng)父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。
虛擬機(jī)會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有好事很長的操作,就可能造成多個線程阻塞,在實際應(yīng)用中這種阻塞往往是隱藏的。
package com.xdwang.demo;
?
public class DealLoopTest {
? ? static class DeadLoopClass {
? ? ? ? static {
? ? ? ? ? ? if (true)// 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally”錯誤
? ? ? ? ? ? {
? ? ? ? ? ? ? ? System.out.println(Thread.currentThread() + "init DeadLoopClass");
? ? ? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
?
? ? public static void main(String[] args) {
? ? ? ? Runnable script = new Runnable() {
? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? System.out.println(Thread.currentThread() + " start");
? ? ? ? ? ? ? ? DeadLoopClass dlc = new DeadLoopClass();
? ? ? ? ? ? ? ? System.out.println(Thread.currentThread() + " run over");
? ? ? ? ? ? }
? ? ? ? };
?
? ? ? ? Thread thread1 = new Thread(script);
? ? ? ? Thread thread2 = new Thread(script);
? ? ? ? thread1.start();
? ? ? ? thread2.start();
? ? }
}
運行結(jié)果:(即一條線程在死循環(huán)以模擬長時間操作,另一條線程在阻塞等待)
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass
需要注意的是,其他線程雖然會被阻塞,但如果執(zhí)行<clinit>()方法的那條線程退出<clinit>()方法后,其他線程喚醒之后不會再次進(jìn)入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。
將上面代碼中的靜態(tài)塊替換如下:
static {
? ? System.out.println(Thread.currentThread() + "init DeadLoopClass");
? ? try {
? ? ? ? TimeUnit.SECONDS.sleep(10);
? ? }
? ? catch (InterruptedException e) {
? ? ? ? e.printStackTrace();
? ? }
}
運行結(jié)果:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
Thread[Thread-0,5,main] run over
Thread[Thread-1,5,main] run over
原因在類加載-時機(jī)的主動引用中已經(jīng)解釋了。
類加載器(class loader)
概念
類加載器(class loader)用來加載 Java 類到 Java 虛擬機(jī)中。一般來說,Java 虛擬機(jī)使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創(chuàng)建出該類的一個對象。
類加載器應(yīng)用在很多方面,比如類層次劃分、OSGi、熱部署、代碼加密等領(lǐng)域。
基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例
java.lang.ClassLoader類
java.lang.ClassLoader類的基本職責(zé)就是根據(jù)一個指定的類的名稱,找到或者生成其對應(yīng)的字節(jié)代碼,然后從這些字節(jié)代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此之外,ClassLoader還負(fù)責(zé)加載 Java 應(yīng)用所需的資源,如圖像文件和配置文件等。
為了完成加載類的這個職責(zé),ClassLoader提供了一系列的方法
方法
說明
getParent()
返回該類加載器的父類加載器。
loadClass(String name)
加載名稱為name的類,返回的結(jié)果是java.lang.Class類的實例。
findClass(String name)
查找名稱為name的類,返回的結(jié)果是java.lang.Class類的實例。
findLoadedClass(String name)
查找名稱為name的已經(jīng)被加載過的類,返回的結(jié)果是java.lang.Class類的實例。
defineClass(String name, byte[] b, int off, int len)
把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類,返回的結(jié)果是 java.lang.Class類的實例。這個方法被聲明為final的。
resolveClass(Class c)
鏈接指定的 Java 類。
類與類加載器
類加載器雖然只用于實現(xiàn)類的加載動作,但它在java程序中起到作用卻遠(yuǎn)遠(yuǎn)不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一起確立其在Java虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。(比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則即使這兩個類來源于同一個Class文件,被同一個虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個類肯定不會相等)
這里說的相等,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結(jié)果,也包括使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等情況。
雙親委派模型
類加載器分類
在虛擬機(jī)的角度上,只存在兩種不同的類加載器:
啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn),是虛擬機(jī)自身的一部分;
其它所有的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機(jī)外部,并且全部繼承自java.lang.ClassLoader
從Java開發(fā)人員的角度看,類加載器還可以劃分得更細(xì)一些,如下:
啟動類加載器(Bootstrap ClassLoader)
這個類加載器負(fù)責(zé)將放置在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定路徑中的,并且是虛擬機(jī)能識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放置在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動類加載器無法被Java程序直接使用。程序員在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器,直接使用null代替即可。
擴(kuò)展類加載器(Extension ClassLoader)
這個類加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴(kuò)展類加載器。
應(yīng)用程序類加載器(Application ClassLoader)
這個類加載器由sum.misc.Launcher.$AppClassLoader來實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
應(yīng)用程序由這三種類加載器互相配合進(jìn)行加載的,如果有必須,還可以加入自己定義的類加載器。這些類加載器之間的關(guān)系一般如下圖
雙親委派模型概念
上圖中展示的類加載器之間的層次關(guān)系,就稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里的類加載器之間的父子關(guān)系一般不會以繼承(Inheritance)的關(guān)系來實現(xiàn),而是使用組合(Composition)關(guān)系來復(fù)用父加載器的代碼。
類加載器的雙親委派模型在JDK1.2期間被引入并廣泛用于之后幾乎所有的Java程序中,但它并不是一個強(qiáng)制性的約束模型,而是Java設(shè)計者推薦給開發(fā)者的一種類加載實現(xiàn)方式。
雙親委派模型的式作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完全這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
雙親委派模型優(yōu)點
Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都會委派給出于模型最頂端的啟動類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類(該類具有系統(tǒng)的Object類一樣的功能,只是在某個函數(shù)稍作修改。比如equals函數(shù),這個函數(shù)經(jīng)常使用,如果在這這個函數(shù)中,黑客加入一些“病毒代碼”。并且通過自定義類加載器加入到JVM中,哈哈,那就熱鬧了),并放在程序的ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,java類型體系中最基礎(chǔ)的行為也就無法保證了,應(yīng)用程序也將變得一片混亂。
雙親委派模型實現(xiàn)
雙親委派模型對于保證Java程序的穩(wěn)定運作很重要,但它的實現(xiàn)卻非常簡單,實現(xiàn)代碼都集中在ClassLoader類默認(rèn)的loadClass方法中。
loadClass默認(rèn)實現(xiàn)如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
? ? ? ? return loadClass(name, false);
}
再看看loadClass(String name, boolean resolve)函數(shù):
protected Class<?> loadClass(String name, boolean resolve)
? ? throws ClassNotFoundException
{
? ? synchronized (getClassLoadingLock(name)) {
? ? ? ? // 1、檢查請求的類是否已經(jīng)被加載過了
? ? ? ? Class c = findLoadedClass(name);
? ? ? ? if (c == null) {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (parent != null) {
? ? ? ? ? ? ? ? ? ? c = parent.loadClass(name, false);
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? c = findBootstrapClassOrNull(name);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (ClassNotFoundException e) {
? ? ? ? ? ? ? ? // 如果父類加載器拋出ClassNotFoundException,說明父類加載器無法完成加載請求
? ? ? ? ? ? }
? ? ? ? ? ? if (c == null) {
? ? ? ? ? ? ? ? // 在父類加載器無法加載的時候,再調(diào)用本身的findClass方法來進(jìn)行類加載
? ? ? ? ? ? ? ? c = findClass(name);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? if (resolve) {
? ? ? ? ? ? resolveClass(c);
? ? ? ? }
? ? ? ? return c;
? ? }
}
檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了,就不需要再加載,直接返回。
如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加載。
如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當(dāng)前類加載器的findClass方法來完成類加載。
換句話說,如果自定義類加載器,就必須重寫findClass方法!
findClass的默認(rèn)實現(xiàn)如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
? ? ? ? throw new ClassNotFoundException(name);
}
可以看出,抽象類ClassLoader的findClass函數(shù)默認(rèn)是拋出異常的。而前面我們知道,loadClass在父加載器無法加載類的時候,就會調(diào)用我們自定義的類加載器中的findeClass函數(shù),因此我們必須要在loadClass這個函數(shù)里面實現(xiàn)將一個指定類名稱轉(zhuǎn)換為Class對象.
如果是讀取一個指定的名稱的類為字節(jié)數(shù)組的話,這很好辦。但是如何將字節(jié)數(shù)組轉(zhuǎn)為Class對象呢?很簡單,Java提供了defineClass方法,通過這個方法,就可以把一個字節(jié)數(shù)組轉(zhuǎn)為Class對象啦~
defineClass主要的功能是:
將一個字節(jié)數(shù)組轉(zhuǎn)為Class對象,這個字節(jié)數(shù)組是class文件讀取后最終的字節(jié)數(shù)組。如,假設(shè)class文件是加密過的,則需要解密后作為形參傳入defineClass函數(shù)。
defineClass默認(rèn)實現(xiàn)如下:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
? ? ? ? throws ClassFormatError ?{
? ? ? ? return defineClass(name, b, off, len, null);
}
函數(shù)調(diào)用過程:
示例
首先,我們定義一個待加載的普通Java類:Test.java。放在com.xdwang.demo包下:
package com.xdwang.demo;
?
public class Test {
? ? public void hello() {
? ? ? ? System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass() + " 加載進(jìn)來的");
? ? }
}
如果你是直接在當(dāng)前項目里面創(chuàng)建,待Test.java編譯后,請把Test.class文件拷貝走,再將Test.java刪除。因為如果Test.class存放在當(dāng)前項目中,根據(jù)雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader 類加載器加載。為了讓我們自定義的類加載器加載,我們把Test.class文件放入到其他目錄。
接下來就是自定義我們的類加載器:
import java.io.FileInputStream;
import java.lang.reflect.Method;
?
public class Main {
? ? static class MyClassLoader extends ClassLoader {
? ? ? ? private String classPath;
? ? ? ? public MyClassLoader(String classPath) {
? ? ? ? ? ? this.classPath = classPath;
? ? ? ? }
? ? ? ? private byte[] loadByte(String name) throws Exception {
? ? ? ? ? ? name = name.replaceAll("\\.", "/");
? ? ? ? ? ? FileInputStream fis = new FileInputStream(classPath + "/" + name
? ? ? ? ? ? ? ? ? ? + ".class");
? ? ? ? ? ? int len = fis.available();
? ? ? ? ? ? byte[] data = new byte[len];
? ? ? ? ? ? fis.read(data);
? ? ? ? ? ? fis.close();
? ? ? ? ? ? return data;
? ? ? ? }
?
? ? ? ? protected Class<?> findClass(String name) throws ClassNotFoundException {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? byte[] data = loadByte(name);
? ? ? ? ? ? ? ? return defineClass(name, data, 0, data.length);
? ? ? ? ? ? } catch (Exception e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? ? ? throw new ClassNotFoundException();
? ? ? ? ? ? }
? ? ? ? }
?
? ? };
?
? ? public static void main(String args[]) throws Exception {
? ? ? ? MyClassLoader classLoader = new MyClassLoader("D:/test");
? ? ? ? //Test.class目錄在D:/test/com/xdwang/demo下
? ? ? ? Class clazz = classLoader.loadClass("com.xdwang.demo.Test");
? ? ? ? Object obj = clazz.newInstance();
? ? ? ? Method helloMethod = clazz.getDeclaredMethod("hello", null);
? ? ? ? helloMethod.invoke(obj, null);
? ? }
}
運行結(jié)果:
恩,是的,我是由 class Main$MyClassLoader 加載進(jìn)來的
破壞雙親委派模型
上面提到過雙親委派模型并不是一個強(qiáng)制性的約束模型,而是java設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式,在java的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現(xiàn)過三次較大規(guī)模的“被破壞”情況。
雙親委派模型的第一次“被破壞”其實發(fā)生在雙親委派模型出現(xiàn)之前--即JDK1.2發(fā)布之前。由于雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則是JDK1.0時候就已經(jīng)存在,面對已經(jīng)存在的用戶自定義類加載器的實現(xiàn)代碼,Java設(shè)計者引入雙親委派模型時不得不做出一些妥協(xié)。為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個新的proceted方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法,因為虛擬在進(jìn)行類加載的時候會調(diào)用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調(diào)用自己的loadClass()。JDK1.2之后已不再提倡用戶再去覆蓋loadClass()方法,應(yīng)當(dāng)把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯里,如果父類加載器加載失敗,則會調(diào)用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派模型的。
雙親委派模型的第二次“被破壞”是這個模型自身的缺陷所導(dǎo)致的,雙親委派模型很好地解決了各個類加載器的基礎(chǔ)類統(tǒng)一問題(越基礎(chǔ)的類由越上層的加載器進(jìn)行加載),基礎(chǔ)類之所以被稱為“基礎(chǔ)”,是因為它們總是作為被調(diào)用代碼調(diào)用的API。但是,如果基礎(chǔ)類又要調(diào)用用戶的代碼,那該怎么辦呢?
這并非是不可能的事情,一個典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動類加載器去加載(在JDK1.3時放進(jìn)rt.jar),但JNDI的目的就是對資源進(jìn)行集中管理和查找,它需要調(diào)用獨立廠商實現(xiàn)部部署在應(yīng)用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代碼,但啟動類加載器不可能“認(rèn)識”之些代碼,該怎么辦?
為了解決這個困境,Java設(shè)計團(tuán)隊只好引入了一個不太優(yōu)雅的設(shè)計:線程上下文件類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程時還未設(shè)置,它將會從父線程中繼承一個;如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,那么這個類加載器默認(rèn)就是應(yīng)用程序類加載器。有了線程上下文類加載器,JNDI服務(wù)使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作,這種行為實際上就是打通了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
雙親委派模型的第三次“被破壞”是由于用戶對程序的動態(tài)性的追求導(dǎo)致的,例如OSGi的出現(xiàn)。在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為網(wǎng)狀結(jié)構(gòu)。
Class.forName()和ClassLoader.loadClass()的區(qū)別
Class.forName(className)方法,內(nèi)部實際調(diào)用的方法是??Class.forName(className,true,classloader);
第2個boolean參數(shù)表示類是否需要初始化,??Class.forName(className)默認(rèn)是需要初始化。
一旦初始化,就會觸發(fā)目標(biāo)對象的 static塊代碼執(zhí)行,static參數(shù)也也會被再次初始化。
ClassLoader.loadClass(className)方法,內(nèi)部實際調(diào)用的方法是??ClassLoader.loadClass(className,false);
第2個 boolean參數(shù),表示目標(biāo)對象是否進(jìn)行鏈接,false表示不進(jìn)行鏈接,由上面介紹可以,
不進(jìn)行鏈接意味著不進(jìn)行包括初始化等一些列步驟,那么靜態(tài)塊和靜態(tài)對象就不會得到執(zhí)行
參考與擴(kuò)展
《深入理解Java虛擬機(jī)》
鏈接:Java類的加載、鏈接和初始化-HollisChuang's Blog
鏈接:深度分析Java的ClassLoader機(jī)制(源碼級別)-HollisChuang's Blog
鏈接:雙親委派模型與自定義類加載器 - ImportNew
鏈接:Java雙親委派模型及破壞 - CSDN博客
---------------------?
作者:Franco蠟筆小強(qiáng)?
來源:CSDN?
原文:https://blog.csdn.net/w372426096/article/details/81901482?
總結(jié)
以上是生活随笔為你收集整理的类加载机制-双亲委派,破坏双亲委派--这一篇全了解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 类加载器的双亲委派及打破双亲委派
- 下一篇: 破坏双亲委派机制的那些事