深入淺出 JVM ClassLoader

# 前言

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

JVM 綜述 里,我們已經大致分析了一些概念。而今天的文章將詳細的闡述類加載器。

首先,我們要了解類加載器,當然,了解的目的是為了更好的開發,通過對類加載器的解讀,看看我們能不能做些什么,比如修改類加載器的加載邏輯,比如加入自定義的類加載器等等功能。

讓我們開始吧!

# 1. 類加載器介紹

對于 Java 虛擬機來說,Class 文件是一個重要的接口,無論使用何種語言進行軟件開發,只要能將源文件編譯為正確的 Class 文件,那么這種語言就可以在 Java 虛擬機上運行。可以說,Class 文件就是虛擬機的基石。

如圖所示:

各種語言都可以在 JVM 上運行

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

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

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

Class 文件轉載過程

其中,只有加載過程是程序員能夠控制的,后面的幾個步驟都是有虛擬機自動運行的。因此,我們的關注點主要放在加載階段。

# 3. 類加載流程中的 “加載”

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

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 類加載器負責加載 <JAVA_HOME>/lib 目錄中的,或者別-Xbootclasspath 參數指定的路徑。并且是被虛擬機識別的,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會加載。

  2. 擴展類加載器有 sun.misc.Launcher$ExtClassLoader 實現,負責加載 <JAVA_HOME>/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!!!!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 原文鏈接:http://iaspecwang.iteye.com/blog/1931043 一.概述 定義:虛擬機...
    晴天哥_王志閱讀 6,810評論 1 35
  • 0、前言 讀完本文,你將了解到: 一、為什么說Jabalpur語言是跨平臺的 二、Java虛擬機啟動、加載類過程分...
    vivi_wong閱讀 1,267評論 0 10
  • ClassLoader翻譯過來就是類加載器,普通的java開發者其實用到的不多,但對于某些框架開發者來說卻非常常見...
    時待吾閱讀 1,102評論 0 1
  • 工作室的任務呢,規定每人不僅看學習視頻,還要寫感想,即學習筆記。周二呢,由于課程比較多,只有再晚上抽出一點時間看了...
    在路上_80f5閱讀 403評論 1 1
  • 2016年的8月19日,郎平閃著淚光接受央視的采訪,就在剛剛,與弟子朱婷的相擁而泣,似乎已經不是那個叱咤風云的“鐵...
    二雙閱讀 231評論 0 0