1. 程式人生 > >類載入機制-雙親委派,破壞雙親委派--這一篇全瞭解

類載入機制-雙親委派,破壞雙親委派--這一篇全瞭解

概述

概念

虛擬機器把描述類的資料從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