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