類加載機制總結

咱們知道java要運行須要編譯和運行,javac將java源代碼編譯爲class文件。而虛擬機把描述類的數據從class文件中加載到內存,並對數據進行校驗、轉換解析、初始化,最終造成能夠被虛擬機直接使用的java類型,這就是類加載機制,他在運行期間完成。java

JVM加載class文件到內存有兩種方式:web

  • 隱式加載:虛擬機自動加載須要的類
  • 顯式加載:代碼中經過調用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);
    }
}
複製代碼

輸出:tomcat

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];
    }
}
複製代碼

輸出:bash

//無輸出
複製代碼

是的,運行以後並無輸出,但他觸發了一個叫「[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的類加載機制須要解決的問題以及是怎麼解決的
  • 瞭解了常見的類加載的異常和大體解決思路
  • 而且也知道了類加載機制大體的代碼實現
  • 最後咱們寫了一個自定義類加載器
相關文章
相關標籤/搜索