深刻淺出 JVM ClassLoader

# 前言

在 JVM 綜述裏面,咱們說,JVM 作了三件事情,Java 程序的內存管理,
Java Class 二進制字節流的加載(ClassLoader),Java 程序的執行(執行引擎)。咱們也說,咱們大部分狀況下只關注前2個。在前面的文章中,咱們已經分析了內存關係相關的,包括運行時數據區,GC 相關。今天咱們要講的就是類加載器。java

JVM 綜述 裏,咱們已經大體分析了一些概念。而今天的文章將詳細的闡述類加載器。程序員

首先,咱們要了解類加載器,固然,瞭解的目的是爲了更好的開發,經過對類加載器的解讀,看看咱們能不能作些什麼,好比修改類加載器的加載邏輯,好比加入自定義的類加載器等等功能。框架

讓咱們開始吧!ide

# 1. 類加載器介紹

對於 Java 虛擬機來講,Class 文件是一個重要的接口,不管使用何種語言進行軟件開發,只要能將源文件編譯爲正確的 Class 文件,那麼這種語言就能夠在 Java 虛擬機上運行。能夠說,Class 文件就是虛擬機的基石。this

如圖所示:spa

各類語言均可以在 JVM 上運行

從上圖能夠看出,虛擬機不拘泥於 Java 語言,任何一個源文件只要能編譯成 Class 文件的格式,就能夠在JVM 上運行!Class 文件格式就像是一個接口,只要遵照這個接口,就可以在 JVM 上運行。線程

# 2. 類加載器的工做流程

Class 文件一般是以文件的方式存在(任何二進制流均可以是 Class 類型),但只有能被 JVM 加載後才能被使用,才能運行編譯後的代碼。系統裝在 Class 類型能夠分爲加載,連接和初始化三個步驟。其中,連接也可分爲驗證,準備和解析3步驟。如圖所示:設計

Class 文件轉載過程

其中,只有加載過程是程序員可以控制的,後面的幾個步驟都是有虛擬機自動運行的。所以,咱們的關注點主要放在加載階段。code

# 3. 類加載流程中的 「加載」

上面說了,類加載器3個流程中,惟一能讓程序員 「作手腳」 的就是加載過程,上面是加載過程呢?其主要做用就是從系統外部得到 Class 二進制數據流。orm

JVM 不會無端裝載 Class 文件,只有在必要的時候才裝載,哪幾個時候呢?

  1. 當建立一個類的實例是,好比使用 new 關鍵字,或者經過反射,克隆,反序列化。
  2. 當調用類的靜態方法時,即當使用字節碼 invokstatic 指令。
  3. 當使用類或接口的靜態字段時(final 常量除外),好比,使用 getstatic 或者 pustatic 指令。
  4. 當時用 Java.lang.reflect 包中的方法反射類的方法時。
  5. 當初始化子類,要求先初始化父類。
  6. 做爲啓動虛擬機,含有 main()方法的那個類。

以上6種狀況屬於主動調用,主動調用會觸發初始化,還有一種狀況是被動調用,則不會引發初始化。

# 3.1 ClassLoader 抽象類介紹

Java 類加載器的具體實現就在 java.lang.ClassLoader,該類是一個抽象類,而且提供了一些重要的接口,用於自定義Class 的加載流程和加載方式。主要方法以下:

  1. public Class<?> loadClass(String name) throws ClassNotFoundException
    給定一個類名,加載一個雷,返回表明這個類的 Class 實例,若是找不到類,則返回異常。

  2. protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
    根據給定的字節碼流 b 定義一個類,off 表示位置,len 表示長度。該方法只有子類可使用。

  3. protected Class<?> findClass(String name) throws ClassNotFoundException
    查找一個類,也是隻能子類使用,這是重載 ClassLoader 時,最重要的系統擴展點。這個方法會被 loadClass 調用,用於自定義查找類的邏輯,若是不須要修改類加載默認機制,只是想改變類加載的形式,就能夠重載該方法。

  4. protected final Class<?> findLoadedClass(String name)
    一樣的,這個方法也只有子類可以使用,他會去尋找已經加載的類,這個方法是 final 方法,沒法被修改。

同時,在該類中,還有一個字段很是重要:parent,他也是一個 ClassLoader 的實例,這個字段所表示的 ClassLoader 也稱爲這個 ClassLoader 的雙親,在類加載的過程當中,ClassLoader 可能會將某些請求交給本身的雙親處理。

# 3.2 類加載器的雙親委派模型

在標準的 Java 程序中,從虛擬機的角度講,只有2種類加載器:

  1. 啓動類加載器(BootStrap ClassLoader),C++ 語言實現,虛擬機自身的一部分
  2. 另外一種就是全部其餘的類加載器,由 Java 語言實現,獨立於虛擬機外部,而且所有繼承自抽身類 java.lang.ClassLoader。

從程序員的角度講,虛擬機會建立 3 中類加載器,分別是:Bootstrap ClassLoader(啓動類加載器),Extension ClassLoader(擴展類加載器)和 APPClassLoader(應用類加載器,也稱爲系統類加載器)。此外,每個應用程序還能夠擁有自定義的 ClassLoader,擴展 Java 虛擬機獲取 Class 數據的能力。

而這 3 個類加載器有着層次關係。

先來看一個著名的圖:

類加載器雙親委派模型

如圖所示:從 ClassLoader 的層次自頂向下爲啓動類加載器,擴展類加載器,應用類加載器和自定義類加載器,當系統須要適用一個類時,在判斷類是否已經被加載時,會先從當前底層類加載器進行判斷,但系統須要加載一個類時,會從頂層類開始加載,依次向下嘗試,直到成功。

注意,咱們沒法訪問啓動類加載器,當試圖獲取啓動類加載器的時候,返回 null,所以,若是返回的是 null,並不意味沒有類加載器爲它服務,而是指哪一個類爲啓動類加載器。

那麼這些類加載路徑是哪些呢?

  1. BootStrap 類加載器負責加載 /lib 目錄中的,或者別-Xbootclasspath 參數指定的路徑。而且是被虛擬機識別的,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會加載。

  2. 擴展類加載器有 sun.misc.Launcher$ExtClassLoader 實現,負責加載 /lib/ext 目錄中的。或者被 java.ext.dirs 系統變量所指定的路徑中的全部類庫。

  3. 應用類加載器由 sun.misc.Launcher$AppClassLoader 實現,因爲這個類是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也稱爲系統類加載器,負載加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器。通常狀況下,這個就是程序中默認的類加載器。

  4. 自定義類加載器用於加載一些特殊途徑的類,通常也是用戶程序類。

系統中的 ClassLoader 在協同工做時,默認會使用雙親委託模式,即在類加載的時候,系統會判斷當前類是否已經被加載,若是已經加載,則直接返回,不然就嘗試加載,在嘗試加載時,會先請求雙親處理,若是雙親查找事變,則本身加載。代碼以下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

代碼中,若是雙親是 null,則使用啓動類加載器加載,若是事變,則使用當前類加載器加載。

雙親爲 null 通常有2種狀況,1. 雙親是啓動類加載器。2. 本身就是啓動類加載器。

其中加載類的邏輯有2個注意的地方。

  1. 判斷是否已經加載?當判斷類是否須要加載時,是從底層開始判斷,若是底層已經加載了,則再也不請求雙親。

  2. 當系統準備加載一個類時。會先從雙親加載,也就是最頂層的啓動類加載器,逐層向下,直到找到該類。和上面的是相反的。

# 3.3 類加載器的雙親委派模型缺陷和補充

雙親模型當然有着優勢,可以讓整個系統保持了類的惟一性。但在有些場合,卻不適合,也就是說,頂層的啓動類加載器的代碼沒法訪問到底層的類加載器。如 rt.jar 沒法中代碼沒法訪問到應用類加載器。

你確定要問,爲何須要訪問呢?

在 Java 平臺中,把核心類(rt.jar)中提供外部服務,可由應用層自行實現的接口,一般能夠稱爲 Service Provider Interface,即 SPI。

在 rt.jar 中的抽象類須要加載繼承他們的在應用層的子類實現,可是以目前的雙親機制是沒法實現的。

所以 JDK 引用了一個不太優雅的設計,上下文類加載器。也就是講類加載放在線程上下文變量中。經過 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 這兩個方法獲取和設置 ClassLoader,這樣,rt.jar 中的代碼就能夠獲取到底層的類加載了。

# 3.4 突破雙親模式

雙親模式是虛擬機的默認行爲,但並不是必須這麼作,經過重載 ClassLoader 能夠修改該行爲。事實上,不少框架和軟件都修改了,好比 Tomcat,OSGI。具體實現則是經過重寫 loadClass 方法,改變類的加載次序。好比先使用自定義類加載器加載,若是加載不到,則交給雙親加載。

# 4. 類加載的擴展---熱替換

咱們知道:由不一樣的 ClassLoader 加載的同名類屬於不一樣的類型,不能相互轉化和兼容。

而這個特性就是咱們實現熱替換的關鍵。過程如圖所示:

熱替換基本思路

# 總結

好了,到這裏,基本的類加載器就介紹結束了。咱們總結了類加載的工做流程,包括加載,鏈接,初始化。而後咱們重點介紹了加載,由於加載階段是咱們程序員惟一有所做爲的地方。而後介紹了加載階段的一些細節,好比雙親委派,而後說了雙親委派的缺點和補充,而後探討了如何修改默認的類加載方式,最後經過類加載的特性實現了熱替換。固然也看了核心類 ClassLoader 的源碼。不過,這確定不是類加載器的所有。咱們將在後面的文章中將類加載的其餘特性一一解開。

good luck!!!!

相關文章
相關標籤/搜索