類載入機制總結

PatrickLee666發表於2019-01-26

我們知道java要執行需要編譯和執行,javac將java原始碼編譯為class檔案。而虛擬機器把描述類的資料從class檔案中載入到記憶體,並對資料進行校驗、轉換解析、初始化,最終形成可以被虛擬機器直接使用的java型別,這就是類載入機制,他在執行期間完成。

JVM載入class檔案到記憶體有兩種方式:

  • 隱式載入:虛擬機器自動載入需要的類
  • 顯式載入:程式碼中通過呼叫ClassLoader類來載入,例如Class.forName()、this.getClass.getClassLoader().loadClass()或者自己實現ClassLoader的findClass()

接下來先來看三個例子

之前的我只知道在物件建立之前會先初始化靜態的東西,也知道從父類開始初始化,但一直不懂為什麼會是這樣的順序,直到我瞭解了虛擬機器是如何實現類載入的。在開始真正瞭解類載入之前,我們先來看三個例子。

第一個

class SuperClass {
    static{
        System.out.println("SuperClass Init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass{
    static{
        System.out.println("SubClass Init");
    }
}

public class NotInitialization{
    public static void main(String agrs[]){
        System.out.println(SubClass.value);
    }
}
複製程式碼

輸出:

SuperClass Init
123
複製程式碼

這道例子似乎很簡單,他告訴我們對於靜態欄位,只有直接定義這個欄位的類才會被初始化,所以,即使這裡是通過子類來引用父類的靜態屬性,他也不會使子類發生初始化,而至於載入和驗證,虛擬機器並沒有明確規範,各步驟的作用下文會談

第二個

class SuperClass {
    static{
        System.out.println("SuperClass Init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass{
    static{
        System.out.println("SubClass Init");
    }
}

public class NotInitialization{
    public static void main(String agrs[]){
        SuperClass[] sca = new SuperClass[10];
    }
}
複製程式碼

輸出:

//無輸出
複製程式碼

是的,執行之後並沒有輸出,但他觸發了一個叫“[Lorg.fenixsoft.classloading.SuperClass”的類初始化,而建立動作由位元組碼指令newarray觸發,從這裡,我們也就直到建立一個物件陣列的真實情況了

第三個

class ConstClass{
    static{
        System.out.println("ConstClass init");
    }

    public static final String WORD = "Hello";
}

public class NotInitialization{
    public static void main(String agrs[]){
        System.out.println(ConstClass.WORD);
    }
}
複製程式碼

輸出:

Hello
複製程式碼

這裡WORD作為一個常量,他在編譯階段就已經生成,意思是說編譯階段經過常量傳播優化,已經將他儲存到了NotInitialization類的常量池中,以後所有對它的引用都是NotInitialization對常量池的引用,這就是為什麼不初始化類。

類初始化

下面來總結一下五種必須對類初始化的情況:

  • 遇到new,getstatic,putstatic,invokestatic這四條位元組碼指令(後三者可以簡單理解為對靜態屬性或方法的呼叫)
  • 使用java.lang.reflect包的方法對類進行反射呼叫時
  • 初始化類時的父類沒有初始化時初始化父類
  • 虛擬機器啟動時,使用者需要執行的主類(main方法的那個類)
  • JDK1.7動態語言支援時(型別檢查在執行時而不是編譯時,java.lang.invoke包,這裡多說一句,動態語言和反射又有所不同)

以上,都是類第一次發生初始化的情況,而對於介面的初始化,他和類的不同就是隻有在真正使用到父介面的時候才會初始化父介面。

類載入過程

下面來具體看一下類載入的全過程分別要做哪些事情

image

載入

這個時期需要完成三件事:

  1. 通過一個全限定名來獲取一個此類的二進位制流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  3. 在記憶體生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問介面

這裡,非陣列類的載入階段和陣列類有些不同:

  • 對於非陣列類
    • 載入階段可以通過系統提供的引導類載入器完成,也可以由使用者自定義的類載入器去完成,也可自己控制位元組流的獲取方式(重寫一個類載入去的loadClass方法)
  • 對於陣列類
    • 若陣列元件型別是引用型別,陣列在載入該元件型別的類載入器的類名稱空間上被標識
    • 若元件型別不是引用型別,將把陣列標記為與引導類載入器關聯
    • 陣列類的可見性和它的元件型別可見性一致,則預設為public

說直白載入的作用就是找到.class檔案並把這個檔案包含的位元組碼讀取到記憶體中

驗證

這一步的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,且不會危害虛擬機器自身的安全,大概分為四部驗證

  • 檔案格式驗證:是否符合Class檔案格式的規範,且能被當前版本虛擬機器處理,保證輸入的位元組流能正確地解析並儲存與方法區之內,格式上符合一個Java型別資訊的要求
  • 後設資料驗證:對類的後設資料資訊進行語義校驗,保證在不符合Java語言規範的後設資料資訊
  • 位元組碼驗證:通過資料流和控制流分析,確定程式語義合法,符合邏輯
  • 符號引用驗證:確保解析動作正常執行,若無法通過符號引用驗證,丟擲java.lang.IncompatibleClassChangeError異常的子類

準備

為類變數分配記憶體並設定類變數初始化值,在方法區進行分配,如int為0,boolean為false,reference為null

解析

將常量池內的符號引用替換為直接引用的過程

問,什麼是符號引用,什麼是直接引用?

我的理解:

符號引用就是一個字串,這個字串有足夠的資訊可以找到相應的位置。直接引用就是偏移量,通過偏移量可以直接在記憶體區域找到方法位元組碼的起始位置。

解析主要包括對類、介面、欄位、類方法、介面方法、方法型別、方法控制程式碼、呼叫點限定符這些符號引用進行

初始化

在類中包含的靜態初始化器都被執行,在這一階段末尾靜態欄位被初始化為預設值,初始化遵守下面幾條原則(其中是類初始化的位元組碼指令)

  • 靜態初始化塊中只能訪問到定義在靜態語句塊之前的變數;定義在他之後的變數,在前面的靜態語句塊可以賦值,不能訪問
  • 虛擬機器保證在父類的在子類的之前執行
  • 虛擬機器保證一個類的方法在多執行緒環境被正確的加鎖、同步

下面來看幾個例子

public class Test {
    static {
        i = 0;
        //System.out.println(i);
    }
    static int i;
}
複製程式碼

上面註釋的那一行會報錯,因為在靜態初始化塊中只能訪問到定義在靜態語句塊之前的變數;定義在他之後的變數,在前面的靜態語句塊可以賦值,不能訪問,說明了第一條

public class Test {
    static class DeadLoopClass{
        static{
            if (true){
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while(true){
                }
            }
        }
    }

    public static void main(String agrs[]){
        Runnable script = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + "run over");
            }
        };
        Thread t1 = new Thread(script);
        Thread t2 = new Thread(script);
        t1.start();
        t2.start();
    }
}
複製程式碼

輸出

Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
複製程式碼

他會列印上面的語句並會發生阻塞,這個例子說明了初始化的時候會保證類會被正確加鎖

類載入器

接下來我們具體看一下類載入器有哪些特點,它的作用就是動態載入類到Java虛擬機器的記憶體空間中,就是上文說的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”,並且這個動作是放到Java虛擬機器外部實現的,就是說應用程式自己決定如何去獲取需要的類

類與類載入器

在JVM中標識兩個class物件是否為同一個類物件存在兩個必要條件

  • 類的完整類名必須一致,包括包名
  • 載入這個類的ClassLoader(指ClassLoader例項物件)必須相同
什麼是類載入的動態性?

一個應用程式總是由n多個類組成,Java程式啟動時,並不是一次把所有的類全部載入後再執行,它總是先把保證程式執行的基礎類一次性載入到jvm中,其它類等到jvm用到的時候再載入,這樣的好處是節省了記憶體的開銷

雙親委派模型

類載入器可以大致分為三類:

  • 啟動類載入器(Bootstrap ClassLoader):這個載入器是C++寫的,他在Java虛擬機器啟動後初始化,負責載入%JAVA_HOME%/jre/lib,-Xbootclasspath引數指定的路徑以及%JAVA_HOME%/jre/classes中的類
  • 擴充套件類載入器(Extension ClassLoader):由sum.misc.Launcher$ExtClassLoader實現,負責載入%JAVA_HOME%/jre/lib/ext,此路徑下的所有classes目錄以及java.ext.dirs系統變數指定的路徑中類庫
  • 應用程式類載入器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader實現,負責載入使用者類路徑上所指定的類庫,父類為ExtensionClassLoader
  • 自定義類載入器
    image

那麼什麼是雙親委派模型呢?我們先來看一下他的工作過程。

如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行,如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器,如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式。

注意,這裡叫雙親不是因為繼承關係而是組合關係

雙親委派模型的好處

很容易想到,雙親委派模型的層級可以避免重複載入,尤其是java的核心類庫不會被替換,例如自己定義了一個java.lang.Integer,雙親委派模型不會去初始化他,而是直接返回載入過的Integer.class。當然,如果強行用defineClass()方法(這個方法將byte位元組流解析成JVM能夠識別的Class物件)去載入java.lang開頭的類也不會成功,會丟擲安全異常

雙親委派模型程式碼實現

ClassLoader的loadClass(),只列出了關鍵的

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        //首先,檢查請求的類是否已經被載入過了
        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){
            //使用類的Class物件建立完成也同時被解析
            resolveClass(c);
        }
        return c;
    }
複製程式碼

ClassLoader的findClass(),

//直接丟擲異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}
複製程式碼

ClassLoader的defineClass

protected Class<?> findClass(String name) throws ClassNotFoundException {
        //獲取類的class檔案位元組陣列
        byte[] classData = getClassData(name);
        if (classData == null){
            throw new ClassNotFoundException();
        } else {
            //直接生成class物件
            return defineClass(name,classData,0,classData.length);
        }
    }
複製程式碼

ClassLoader的resolveClass()

protected final void resolveClass(Class<?> c) {
        if (c == null) {
            throw new NullPointerException();
        }
    }
複製程式碼

下面再來看一下關鍵方法的具體作用:

  • loadClass():該方法載入指定名稱(包括包名)的二進位制型別,resolve引數代表載入同時是否需要被解析
  • findClass():自定義的類載入邏輯寫在findClass()方法中
  • defineClass():用來將byte位元組流解析成JVM能夠識別的Class物件(ClassLoader中已實現該方法邏輯)
  • resolveClass():該方法可以使用類的Class物件建立完成也同時被解析

先看以下loadClass()方法,通過以上程式碼可以看到邏輯並不複雜:先檢查是否已經被載入過,若沒有載入則呼叫父載入器的loadClass(),若父載入器為空讓啟動類載入器為父載入器,若父類載入失敗,丟擲異常,再呼叫自己的findClass()方法

在JDK1.2之後,如果我們自定義類載入器的話我們將不再重寫loadClass(),因為ClassLoader已經實現loadClass(),並且用它來達到雙親委派的效果。我們自定義類載入器需要重寫的是findClass(),知道findClass()方法是在loadClass()方法中被呼叫的,當loadClass()方法中父載入器載入失敗後,則會呼叫自己的findClass()方法來完成類載入,這樣就可以保證自定義的類載入器也符合雙親委託模式。

破壞雙親委派模型

雙親委派模型不是一個強制性的約束模型,雙親委派模型也有不太適用的時候,這時根據具體的情況我們就要破壞這種機制,雙親委派模型主要出現過三次被破壞的情況

第一次:

因為雙親委派模型是在JDK1.2的時候出現的,所以,在JDK1.2之前,是沒有雙親委派的,為了向前相容,JDK1.2之後的java.lang.ClassLoader新增了一個新的protected的findClass()方法,這個方法的唯一邏輯就是呼叫自己的loadClass(),前文分析程式碼實現的時候我們知道雙親委派模型就是根據loadClass()來實現的,所以為了使用雙親委派模型,我們應當把自己的類載入邏輯寫道findClass()中。

第二次:

我們有一些功能是java提供介面,而其他的公司提供實現類,例如我們的JDBC、JNDI(由多個公司提供自己的實現)所以像JDBC、JNDI這樣的SPI(服務提供者介面),就需要第三方實現,這些SPI的介面屬於核心庫,由Bootstrap類載入器載入,那麼如何去載入那些公司提供的實現類呢?這就是我們的執行緒上下文類載入器,下圖是整體大概的工作流程

image
這裡,執行緒上下文載入器預設是父類載入器是ApplicationClassLoader

第三次:

第三次破壞委派雙親模型就是由於使用者追求動態性導致的,“動態性”就是指程式碼熱替換、模組熱部署等,就是希望程式不需要重啟就可以更新class檔案,最典型的例子就是SpringBoot的熱部署和OSGi。這裡拿OSGi舉例,OSGi實現模組化熱部署的關鍵就是它自定義類載入機制的實現,每一個程式模組(OSGi中稱為Bundle)都有自己的類載入器,當需要更換一個Bundle時,就把Bundle連同類載入器一起換掉實現熱部署

所以,在OSGi環境下,類載入器不再是層次模型,而是網狀模型,如圖

image

當OSGi收到一個類載入的時候會按照以下的順序進行搜尋:

  • 將以java.*開頭的類委派給父類載入器載入
  • 否則,將委派列表名單內的類委派給父類載入器載入
  • 否則,將Import列表中的類委派給Export這個類的Bundle的類載入器載入
  • 否則,查詢當前Bundle的ClassPath,使用自己的類載入器載入
  • 檢查Fragment Bundle中是否可以載入
  • 查詢Dynamic Import列表的Bundle
  • 若以上都沒有進行類載入,則載入失敗

以上前兩點仍符合雙親委派規則,其餘都是平級類載入器查詢

Tomcat的類載入器模式

前文我們瞭解了Java中類載入器的執行方式;但主流的Web伺服器都會有自己的一套類載入器,為什麼呢?因為對於伺服器來說他要自己解決一些問題:

  • 部署在同一個Web容器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離。兩個不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個伺服器中只有一份,伺服器應當保證兩個應用程式的類庫可以互相獨立使用。
  • 部署在同一個Web容器上的兩個Web應用程式所使用的相同的類庫相同的版本可以互相共享。例如,使用者可能有10個使用Spring組織的應用程式部署在同一臺伺服器上,如果把10份Spring分別存放在各個應用程式的隔離目錄中,將會是很大的資源浪費——這主要倒不是浪費磁碟空間的問題,而是指類庫在使用時都要被載入到Web容器的記憶體,如果類庫不能共享,虛擬機器的方法區就會很容易出現過度膨脹的風險
  • Web容器需要儘可能地保證自身的安全不受部署的Web應用程式影響。Web容器也有用Java實現的,那麼肯定不能把Web容器的類庫和程式的類庫弄混
  • 支援jsp的web容器,要支援熱部署。我們知道執行jsp時實際上會先將jsp翻譯成servlet,再編譯為.class再在虛擬機器執行起來再返回給客戶端。而我們在編寫jsp時,當tomcat伺服器正在執行的時候,我們直接在jsp中修改程式碼時並不需要重啟伺服器,這就是達到了動態載入類的效果。

顯然,如果Tomcat使用預設的類載入機制是無法滿足上述要求的

  1. 無法載入兩個相同類庫的不同版本的,因為預設類載入只在乎許可權定類名,第一條不行
  2. 可以實現
  3. 預設類載入只在乎許可權定類明,所以第三條不行
  4. 前文我們說過,JVM確定是否為同一個類物件會要求類和類載入器都相同,預設的肯定不行,但我們可以想到當改變jsp程式碼的時候就改一次類載入器

接下來來看Tomcat的類載入器:

image
++一個WebAppClassLoader下可能還對應多個JspClassLoader++

再來說說Tomcat的目錄結構:

  • /common目錄中:類庫可被Tomcat和所有的Web應用程式共同使用。
  • /server目錄中:類庫可被Tomcat使用,對所有的Web應用程式都不可見。
  • /shared目錄中:類庫可被所有的Web應用程式共同使用,但對Tomcat自己不可見。
  • /WebApp/WEB-INF目錄中:類庫僅僅可以被此Web應用程式使用,對Tomcat和其他Web應用程式都不可見。

再來看一下具體每個類載入器的載入流程:

CommonClassLoader能載入的類都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能載入的類則與對方相互隔離。WebAppClassLoader可以使用SharedClassLoader載入到的類,但各個WebAppClassLoader例項之間相互隔離。而JasperLoader的載入範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丟棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的例項,並通過再建立一個新的Jsp類載入器來實現JSP檔案的HotSwap功能。

Tomcat 6.x把/common、/server和/shared三個目錄預設合併到一起變成一個/lib目錄,這個目錄裡的類庫相當於以前/common目錄中類庫的作用

現在我們再來看Tomcat時如何解決之前的四個問題的:

  • 部署在同一個Web容器上的兩個Web應用程式所使用的Java類庫可以實現相互隔離:各個WebAppClassLoader例項之間相互隔離
  • 部署在同一個Web容器上的兩個Web應用程式所使用的相同的類庫相同的版本可以互相共享:可以放在Common或Shared目錄下讓這些程式共享
  • Web容器需要儘可能地保證自身的安全不受部署的Web應用程式影響:CatalinaClassLoader載入web伺服器需要的類庫,WebAppClassLoader只能得到SharedClassLoader的類庫
  • 支援jsp的web容器,要支援熱部署:每當改變jsp時,更新JasperClassLoader
問:前文說到如果我們伺服器上有十個Spring組織的程式,我們可以把Spring放到Common或者Shared目錄下共享,但Spring要進行類管理肯定要訪問到使用者程式,即訪問到不在他載入範圍的使用者程式,這要怎麼實現呢?

前文我們說過破壞委託模型,這裡就是一個例子,可以採用執行緒上下文載入器,讓父類載入器請求子類載入器完成載入類作用

常見載入類異常錯誤分析

ClassNotFoundException

這個錯誤是說當JVM載入指定檔案的位元組碼到記憶體時,找不到相應的位元組碼。解決辦法為在當前classpath目錄下找有沒有指定檔案(this.getClass().getClassLoader().getResource("").toString()可以檢視當前classpath)

NoClassDefFoundError

這種錯誤出現的情況就是使用了new關鍵字、屬性引用某個類、繼承某個介面或實現某個類或某個方法引數引用了某個類,這時虛擬機器隱式載入這些類發現這些類不存在的異常。解決這個錯誤的辦法就是確保每個類引用的類都在當前的classpath下面

UnsatisfiedLinkError

可能是在JVM啟動的時候不小心在JVM中的某個lib刪了

ClassCastException

無法轉型,這個可能對於初學者來說會很常見(比如說我,哈哈),解決辦法時轉型前先用instanceof檢查是不是目標型別再轉換

ExceptionInInitializerError

這個異常是由於類載入過程中靜態塊初始化過程失敗所導致的。由於它出現在負責啟動程式的主執行緒中,因此你最好從主類中開始分析,這裡說的主類是指你在命令列引數中指定的那個,或者說是你宣告瞭public static void main(String args[])方法的那個類。這個異常很大可能會伴隨NoClassDefFoundError,所以出現NoClassDefFoundError時我們先看ExceptionInInitializerError出現沒。

自定義類載入器

接下來我們要自己寫一個類載入器,在開始寫之前,我們要知道為什麼需要我們自己寫類載入器呢?

  • 我們需要的類不一定存放在已經設定好的classPath下(有系統類載入器AppClassLoader載入的路徑),對於自定義路徑中的class類檔案的載入,我們需要自己的ClassLoader
  • 有時我們不一定是從類檔案中讀取類,可能是從網路的輸入流中讀取類,這就需要做一些加密和解密操作,這就需要自己實現載入類的邏輯,當然其他的特殊處理也同樣適用。
  • 可以定義類的實現機制,實現類的熱部署,如OSGi中的bundle模組就是通過實現自己的ClassLoader實現的。

下面我們開始自定義類載入器吧

自定義File類載入

package SelfClassLoader;

import java.io.*;

public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir){
        this.rootDir = rootDir;
    }

    /**
     * 編寫findClass方法的邏輯
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //獲取類的class檔案位元組陣列
        byte[] classData = getClassData(name);
        if (classData == null){
            throw new ClassNotFoundException();
        } else {
            //直接生成class物件
            return defineClass(name,classData,0,classData.length);
        }
    }


    /**
     * 編寫獲取class檔案並轉換為位元組碼流的邏輯
     * @param className
     * @return
     */
    private byte[] getClassData(String className){
        //讀取類檔案的位元組
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int bytesNumRead = 0;
            // 讀取類檔案的位元組碼
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 類檔案的完整路徑
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    /**
     * 讀取檔案
     */
    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
        //建立自定義檔案類載入器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //載入指定的class檔案,加上包名
            Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //輸出結果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}
複製程式碼

我們通過getClassData()方法找到class檔案並轉換為位元組流,並重寫findClass()方法,利用defineClass()方法建立了類的class物件。在main方法中呼叫了loadClass()方法載入指定路徑下的class檔案,由於啟動類載入器、擴充類載入器以及系統類載入器都無法在其路徑下找到該類,因此最終將有自定義類載入器載入,即呼叫findClass()方法進行載入。

還有一種方式是繼承URLClassLoader類,然後設定自定義路徑的URL來載入URL下的類,這種方式更常見

package SelfClassLoader;

import java.io.File;
import java.net.*;

public class PathClassLoader extends URLClassLoader {

    private String packageName = "net.lijunfeng.classloader";

    public PathClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public PathClassLoader(URL[] urls) {
        super(urls);
    }

    public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException{
        Class<?> aClass = findLoadedClass(name);
        if (aClass != null){
            return aClass;
        }
        if (!packageName.startsWith(name)){
            return super.loadClass(name);
        } else {
            return findClass(name);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
        String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
        //建立自定義檔案類載入器
        File file = new File(rootDir);
        //File to URI
        URI uri=file.toURI();
        URL[] urls={uri.toURL()};

        PathClassLoader loader = new PathClassLoader(urls);

        try {
            //載入指定的class檔案
            Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //輸出結果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

總結

  • 我們知道了類載入器的作用就是將.class檔案存到記憶體當中去(兩種)
  • 知道了它具體的步驟和每步的功能
  • 瞭解了Java三種類載入器以及雙親委派模型和破壞雙親委派模型
  • 瞭解了Tomcat的類載入機制需要解決的問題以及是怎麼解決的
  • 瞭解了常見的類載入的異常和大致解決思路
  • 並且也知道了類載入機制大致的程式碼實現
  • 最後我們寫了一個自定義類載入器

相關文章