JVM(三)類加載器

類的加載

類的加載是指將類的.class文件中二進制數據讀入到內存中,而後將其放在運行時數據區的方法區內,而後在內存中建立愛你一個java.lang.Class對象java

規範並無說明Class對象應該存放在哪,HotSpot虛擬機將其放在方法區中,用來封裝類在方法區內的數據結構mysql

加載.class文件的方式

  • 從本地系統中直接加載
  • 從網絡下載.calss文件
  • 從zip,jar等歸檔文件中加載
  • 從專有數據庫中提取
  • 將Java源文件動態編譯爲.class文件

servlet技術sql

類加載器

類加載器用來把類加載到Java虛擬機中,從JDK1.2版本開始,類的加載過程採用雙親委託機制,這種機制能保證Java平臺的安全性.數據庫

從源碼文檔中翻譯應該稱爲父類委託模式bootstrap

類加載器並不須要等到某個類被首次主動使用時再加載它數組

  • JVM規範容許類加載器在預料某個類將要被使用時就預先加載它,若是在預先加載的過程當中遇到了.class文件缺失或者存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError)
  • 若是一個類一直沒有被程序主動使用,那麼累加載器就不會報告錯誤

JVM中的類加載器

根加載器(Bootstrap),

根加載器沒有父加載器,主要負責虛擬機的核心類庫,如java.lang.*等,java.lang.Object是由根類加載器加載的,根類加載器的實現依賴於底層操做系統,屬於虛擬機實現第一部分,它並無繼承java.lang.ClassLoader類. 啓動類加載器是特定於平臺的機器指令,它負責開啓整個加載過程 啓動類加載器還會負責加載JRE正常運行所需的基本組件.其中包括java.util,java.lang包中的類安全

擴展類加載器(Extension)

擴展類加載器的父加載器是根加載器,從java.ext.dirs系統屬性指定的目錄中加載類庫,或者再jre\lib\ext子目錄下加載類庫,若是把用戶建立的JAR文件放在這個目錄下,會自動由擴展類加載器加載,擴展類加載器是純Java類,是ClassLoader的子類bash

注意一點的是,拓展類加載器加載的是jar包內的class文件網絡

系統(應用)類加載器(System/Application)

系統類加載器的父加載器爲擴展類加載器,從環境變量classpath或者系統屬性java.class.path所制定的目錄加載類,它是用戶自定義的類加載器的默認父加載器,系統類加載器是純Java類,是ClassLoader的子類數據結構

用戶自定義的類加載器

除了虛擬機自帶的加載器外,用戶能夠定製本身的類加載器.Java提供了抽象類ClassLoader.全部用戶自定義的加載器都應該繼承ClassLoader

AppClassLoader和ExtClassLoader都是Java類,因此須要類加載器進行加載,而這兩個類的類加載器就是bootstrapClassLoader

能夠經過修改 System.getProperty(java.system.class.loader)對默認的SystemClassLoader進行修改

類加載器的層級關係

父親委託機制

在父親委託機制中,各個加載器按照父子關係造成樹形結構,除了根加載器以外,其他的類加載器有且只有一個父加載器.

父親委託機制

簡單描述,就是一個類加載器要加載一個類,並非由自身進行直接加載,而是經過向上尋找父加載器,直到沒有父加載器的類加載器,而後再從上至下嘗試加載,直至找到一個能夠正確加載的類加載器,通常狀況下,系統類加載器就能加載普通的類.

並非全部的類加載器都必須遵照雙親委託的機制,具體實現能夠根據須要進行改造

代碼示例,查看類的加載器

public class Test08 {

    public static void main(String[] args) {
        try {
            Class<?> clzz = Class.forName("java.lang.String");
            //若是返回null,證實是由BootStrap加載器進行加載的
            System.out.println(clzz.getClassLoader());


            Class<?> customClass = Class.forName("com.r09er.jvm.classloader.Custom");
            System.out.println(customClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Custom{

}
複製代碼

輸出

null
sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼

String的類加載器爲null,證實String是由Bootstrap類加載器加載,由於根加載器是由C++實現.因此會返回null.

Custom的類加載器是Launcher$AppClassLoader,這個類是不開源的.可是是默認的系統(應用)類加載器.

classLoader和初始化的時機

經過ClassLoader手動加載類,觀察是否會觸發類的初始化

public class Test12 {

    public static void main(String[] args) throws Exception {
        ClassLoader loader  = ClassLoader.getSystemClassLoader();
        Class<?> aClass = loader.loadClass("com.r09er.jvm.classloader.TestClassLoader");

        System.out.println(aClass);

        System.out.println("-------");

        aClass = Class.forName("com.r09er.jvm.classloader.TestClassLoader");

        System.out.println(aClass);

    }
}
class TestClassLoader{
    static {
        System.out.println("Test classloader");
    }
}
複製代碼

輸出

class com.r09er.jvm.classloader.TestClassLoader
-------
Test classloader
class com.r09er.jvm.classloader.TestClassLoader
複製代碼

結論

明顯能夠看出,classLoader.load方法加載類,類並不會初始化,說明不是對類的主動使用,調用了Class.ForName才進行初始化

不一樣的類加載器與加載動做分析

打印類加載器,因爲根加載器由C++編寫,因此就會返回null

public static void main(String[] args) {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        System.out.println(loader);
        //向上遍歷父classLoader
        while (null != loader) {
            loader = loader.getParent();
            System.out.println(loader);
        }
    }
複製代碼

輸出結果

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null

複製代碼

獲取ClassLoader的途徑

  • 經過類對象獲取ClassLoader,clazz.getClassLoader()
  • 經過線程獲取上限文ClassLoader,Thread.currentThread().getContextLoader()
  • 得到系統(應用)ClassLoader,ClassLoader.getSystemClassLoader()
  • 得到調用者的ClassLoader,DriverManager.getClassLoader()

ClassLoader源碼分析

JavaDoc描述

類加載器是負責加載的對象,classLoader是抽象類.賦予類一個二進制名稱,一個類加載器應當嘗試定位生成數據,這些數據構成類的定義.一種典型的策略是將二進制名稱轉換爲文件名,而後從文件系統中讀取該名稱的字節碼文件

每個對象都包含定義該的classLoader引用(reference)

數組對應的class對象並非由類加載器建立的,而是由java虛擬機在須要時自動建立的.對於一個數組的類加載器,與這個數組元素的類加載器一致.若是數組是原生類型,那這個數組將沒有classLoader

String[],則這個數組的類加載器是String的類加載器,使用的是Bootstrap類加載器 int[] ,這種基本類型的數組,是沒有類加載器的.

應用實現classLoader的目的是爲了拓展JVM動態加載類

ClassLoader使用了委託模型去尋找類的資源.ClassLoader的每個實例都有會一個關聯的父ClassLoader,當須要尋找一個類的資源時,ClassLoader實例就會委託給父ClassLoader.虛擬機內建的ClassLoader稱爲BootstrapClassLoader,BootstrapClassLoader自己是沒有父ClassLoader的,可是能夠做爲其餘ClassLoader的父加載器

支持併發加載的類加載器稱爲並行類加載器,這種類加載器要求在類初始化期間經過ClassLoader.registerAsParallelCapable將自身註冊上.默認狀況下就是並行的,而子類須要須要並行,則須要調用該方法

在委託機制並非嚴格層次化的環境下,classLoader須要並行處理,不然類在加載過程當中會致使死鎖,由於類加載過程當中是持有鎖的

一般狀況下,JVM會從本地的文件系統中加載類,這種加載與平臺相關.例如在UNIX系統中,jvm會從環境變量中CLASSPATH定義的目錄中加載類.

然而有些類並非文件,例如網絡,或者由應用構建出來(動態代理),這種狀況下,defineClass方法會將字節數組轉換爲Class實例,能夠經過Class.newInstance建立類真正的對象 由類加載器建立的對象的構造方法和方法,可能會引用其餘的類,因此JVM會調用loadClass方法加載其餘引用的類

二進制名稱BinaryNames,做爲ClassLoader中方法的String參數提供的任何類名稱,都必須是Java語言規範所定義的二進制名稱。 例如

  • "java.lang.String",全限定類名
  • "javax.swing.JSpinner$DefaultEditor",內部類
  • "java.security.KeyStoreBuilderFileBuilder$1",匿名內部類
  • "java.net.URLClassLoader$3$1"

自定義類加載器

步驟

  • 1.繼承CLassLoader
  • 2.重寫loadClass方法
  • 3.在loadClass方法中實現加載class字節碼的方法,返回byte[]
  • 4.調用super.defineClass(byte[])方法將Class對象返回給loadClass方法

源碼示例

public class Test16 extends ClassLoader {

    private String classLoaderName;

    private String path;

    private final String fileExtension = ".class";


    public Test16(String classLoaderName) {
        //將systemClassLoader做爲當前加載器的父加載器
        super();
        this.classLoaderName = classLoaderName;
    }

    public Test16(ClassLoader parent, String classLoaderName) {
        //將自定義的ClassLoader做爲當前加載器的父加載器
        super(parent);
        this.classLoaderName = classLoaderName;
    }


    public void setPath(String path) {
        this.path = path;
    }

    public static void main(String[] args) throws Exception {
        Test16 loader1 = new Test16("loader1");
        //設置絕對路徑,加載工程根目錄下的com.r09er.jvm.classloader.Test01.class
        loader1.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
        Class<?> aClass = loader1.loadClass("com.r09er.jvm.classloader.Test01");
        //打印加載的類
        System.out.println("loader1 load class" + aClass.hashCode());
        Object instance = aClass.newInstance();
        System.out.println("instance1: " + instance);


        Test16 loader2 = new Test16("loader2");
        //設置絕對路徑,加載工程根目錄下的Test01.class
        loader2.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
        Class<?> aClass2 = loader2.loadClass("com.r09er.jvm.classloader.Test01");
        System.out.println("loader2 load class" + aClass2.hashCode());
        Object instance2 = aClass2.newInstance();
        System.out.println("instance2 : " + instance2);

        //todo ****
        //1.從新編譯工程,確保默認的classPath目錄下有Test01.class的字節碼文件,而後運行main方法,觀察輸出
        //2.刪除默認classpath目錄下的Test01.class,運行main方法,觀察輸出

    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("invoke findClass");
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] bytes = this.loadClassData(name);
        return super.defineClass(name, bytes, 0, bytes.length);
    }

    private byte[] loadClassData(String binaryName) {
        byte[] data = null;

        binaryName = binaryName.replace(".", "/");

        try (
                InputStream ins = new FileInputStream(new File(this.path + binaryName + this.fileExtension));
                ByteArrayOutputStream baos = new ByteArrayOutputStream();

        ) {
            int ch;
            while (-1 != (ch = ins.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

}

複製代碼

執行兩次main方法後,會發現類加載器真正生效的邏輯,由於默認的父加載器實際上是系統加載器(AppClassLoader),因此若是默認的classPath存在字節碼文件,則會由AppClassLoader正確加載類,若是classPath中沒有,則會向下使用自定義的類加載器加載類

若是構造函數傳入兩個不同的ClassLoaderName,會發現兩個class對象並不一致,是因爲命名空間NameSpace的緣由,由於兩個類加載器定義的名稱是不同的,若是改爲相同的名稱,則兩個class對象一致

重寫的是findClass方法,在調用時候,使用的是classLoader的loadClass方法,這個方法內部會調用findClass

還有一個重點,若是將class字節碼文件放在根目錄,則會拋出NoClassDefFoundError異常,由於binaryName不符合規範.

自定義類加載器加載類流程圖

類加載器重要方法詳解

findClass

實現本身的類加載器,最重要就是實現findClass,經過傳入binaryName,將二進制名稱加載成一個Class對象

defineClass

在實現findClass後,須要經過defineClass方法,將二進制數據交給defineClass方法轉換成一個Class實例, 在defineClass內部會作一些保護和檢驗工做.

雙親委派機制解析

經過loadClass方法加載類,會有以下默認加載順序

  • 1.調用findLoadedClass方法檢查class是否被加載
  • 2.調用父加載器的loadClass方法,若是父加載器爲null,則會調用JVM內建的類加載器.
  • 3.調用findClass方法找到類

在默認的loadClass方法中,類加載是同步

雙親委派機制優勢

  • 1.能夠確保Java核心類庫的類型安全,若是這個加載過程由Java應用本身的類加載器完成,極可能會在JVM中存在多個版本的同一個類(包名,類名一致),

命名空間發揮的做用

  • 2.能夠確保Java核心類庫提供的類不會被自定義的類替代

由於優先加載的是類庫中的class,會忽略掉自定義的類

  • 3.不一樣的類加載器能夠爲相同名稱(binaryName)的類建立額外的命名空間,相同名稱的類能夠並存在JVM中.

類的卸載

當類被加載,鏈接,初始化以後,它的生命週期就開始了.當表明類的Class對象再也不被引用,即不可觸及時,Class對象就會結束生命週期,類在元空間內的數據也會被卸載,從而結束類的生命週期.

一個類什麼時候結束生命週期,取決於表明它的Class對象什麼時候結束生命週期

由Java虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載.

用戶自定義的類加載器所加載的類是能夠被卸載的

類加載器加載的類路徑

BootstrapClassLoader加載的路徑

  • System.getProperty("sun.boot.class.path")

ExtClassLoader

  • System.getProperty("java.ext.dirs")

AppClassLoader

  • System.getProperty("java.class.path")

三個路徑和JDK版本,操做系統都有關係

若是將編譯好的class字節碼文件放到根加載器的加載路徑上,能夠成功由BootstrapClassLoader加載

類加載器命名空間

  • 每一個類加載器都有本身的命名空間,命名空間由該加載器及全部父加載器所加載的類組成

即子加載器能訪問父加載器加載的類,而父加載器不能訪問子加載器加載的類.(相似於繼承的概念)

  • 在同一個命名空間中,不會出現類的完整名字相同的兩個類

一個Java類是由該類的全限定名稱+用於加載該類的定義類加載器(defining loader)共同決定.

ClassLoader.getSystemClassLoader源碼

返回用於委託的系統類加載器.是自定義類加載器的父加載器,一般狀況下類會被系統類加載器加載. 該方法在程序運很早的時間就會被建立,而且會將系統類加載器設爲調用線程的上下文類加載器(context class loader)

Launcher構造主要邏輯

1.初始化ExtClassLoader 2.初始化AppClassLoader,將初始化好的ExtClassLoader設置爲AppClassLoader的父加載器 3.將AppClassLoader設置爲當前線程的上下文類加載器

SystemClassLoaderAction邏輯

1.判斷System.getProperty("java.system.class.loader")是否有設置系統類加載器 2.若是爲空,直接返回AppClassLoader 3.若是不爲空,經過反射建立classLoader,其中必須提供一個函數簽名爲ClassLoader的構造 4.將反射建立的自定義類加載器設置爲上限爲加載器. 5.返回建立好的類加載器

Class.ForName(name,initialize,classloader)解析

  • name,須要構造的類全限定名稱(binaryName)

不能用於原生類型或者void類型 若是表示的是數組,則會加載數組中的元素class對象,可是不進行初始化

  • initialize,類是否須要初始化
  • classloader,加載此類的類加載器

線程上下文加載器(ContextClassLoader)實現與分析

CurrentClassLoader(當前類加載器)

  • 每個類都會嘗試使用本身的ClassLoader去加載當前類引用的其餘類

若是ClassA引用了ClassY,那麼ClassA的類加載器會去加載ClassY,前提是ClassY未被加載

線程類加載器從JDK1.2開始引入,Thread類中的getContextClassLoadersetContextClassLoader分別用來獲取和設置上下文加載器.若是沒有手動進行設置,那麼線程會繼承其父線程的上下文加載器. java應用運行時的初始線程的上下文類加載器是系統類加載器(AppClassLoader),在線程中運行的類能夠經過這個類加載器加載類與資源

由JDBC引出的問題

回顧一下JDBC操做

Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.connect();
Statement stae = conn.getStatement();
複製代碼

Driver,Connection,Statement都是由JDK提供的標準,而實現是由具體的DB廠商提供. 根據類加載的機制,JDK的rt包會被BootstrapClassLoader加載,而自定義的類會被AppClassLoader加載,同時由於命名空間的緣由,父加載器是沒法訪問子加載器加載的類的.因此父加載器會致使這個問題.

上下文加載器就是爲了解決這種問題所存在的

父ClassLaoder可使用當前線程Thread.currentThread().getContextClassLoader()加載的類, 這就改變了父ClassLoader不能使用子ClassLoader或是其餘沒有直接父子關係的ClassLoader沒法訪問對方加載的class問題.

即改變了父親委託模型

線程上下文加載器通常使用

使用步驟(獲取 - 使用 - 還原)

  1. Thread.currentThread().getContextClassLoader()
  2. Thread.currentThread().setContextClassLoader(targetClassLoader) doSomentthing(); 3.Thread.currentThread().setContextClassLoader(originClassLoader);

ContextClassLoader的做用就是破壞Java的類加載委託機制

ServiceLoader

ServiceLoader是一個簡單的服務提供者加載設施

加載基於JDK規範接口實現的具體實現類 實現類須要提供無參構造,用於反射構造出示例對象

服務提供者將配置文件放到資源目錄的META-INF/services目錄下,告訴JDK在此目錄的文件內配置了須要加載的類,其中文件名稱是須要加載的接口全限定名稱,文件內容是一個或多個實現的類全限定名稱.

總結

在雙親委託模型下,類加載時由下至上的.可是對於SPI機制來講,有些接口是由Java核心庫提供的,根據類加載的機制,JDK的rt包會被BootstrapClassLoader加載,而自定義的類會被AppClassLoader加載.這樣傳統的雙親委託模型就不能知足SPI的狀況,就能夠經過線程上下文加載器來實現對於接口實現類的加載.

相關文章
相關標籤/搜索