【JAVA】類加載器

ClassLoader的簡單介紹

Class的裝載大體上可以分為加載類、連接類和初始化三個階段,在這三個階段中,所有的Class都是由ClassLoader進行加載的,然后Java虛擬機負責連接、初始化等操作.也就是說,無法通過ClassLoader去改變類的連接和初始化行為.
Java虛擬機會創建三類ClassLoader,分別是

BootStrap ClassLoader(啟動類加載器)
Extension ClassLoader(擴展類加載器)
APP ClassLoader(應用類加載器,也稱為系統類加載器)

此外,每個應用還可以自定義ClassLoader

ClassLoader的雙親委托模式

在ClassLoader的結構中,還有一個重要的字段parent,它也是一個ClassLoader的實例,這個字段字段表示的ClassLoader也成為這個ClassLoader的雙親,在類加載的過程中,可能會將某些請求交于自己的雙親處理.
如圖,應用類加載器的雙親為擴展類加載器,擴展類加載器的雙親為啟動類加載器.

屏幕快照 2019-01-25 下午12.30.52.png

系統中的ClassLoader在協同工作時,默認會使用雙親委托模式.即在類加載的時候,系統會判斷當前類是否已經被加載,如果被加載,就會直接返回可用的類,否則就會嘗試加載,在嘗試加載時,會先請求雙親處理,如果雙親請求失敗,則會自己加載.

雙親委托模式的弊端

判斷類是否加載的時候,應用類加載器會順著雙親路徑往上判斷,直到啟動類加載器.但是啟動類加載器不會往下詢問,這個委托路線是單向的,即頂層的類加載器,無法訪問底層的類加載器所加載的類,如圖

屏幕快照 2019-01-25 下午12.32.32.png

啟動類加載器中的類為系統的核心類,比如,在系統類中,提供了一個接口,并且該接口還提供了一個工廠方法用于創建該接口的實例,但是該接口的實現類在應用層中,接口和工廠方法在啟動類加載器中,就會出現工廠方法無法創建由應用類加載器加載的應用實例問題.
擁有這樣問題的組件有很多,比如JDBC、Xml parser等.JDBC本身是java連接數據庫的一個標準,是進行數據庫連接的抽象層,由java編寫的一組類和接口組成,接口的實現由各個數據庫廠商來完成

雙親委托模式的補充

在Java中,把核心類(rt.jar)中提供外部服務,可由應用層自行實現的接口,這種方式成為spi.那我們看一下,在啟動類加載器中,訪問由應用類加載器實現spi接口的原理

Thread類中有兩個方法

public ClassLoader getContextClassLoader()//獲取線程中的上下文加載器
public void setContextClassLoader(ClassLoader cl)//設置線程中的上下文加載器

通過這兩個方法,可以把一個ClassLoader置于一個線程的實例之中,使該ClassLoader成為一個相對共享的實例.這樣即使是啟動類加載器中的代碼也可以通過這種方式訪問應用類加載器中的類了.如下圖


屏幕快照 2019-01-25 下午12.36.48.png
  1. Java 的類加載器的種類都有哪些?

1、根類加載器(Bootstrap) --C++寫的 ,看不到源碼
2、擴展類加載器(Extension) --加載位置 :jre\lib\ext 中
3、系統(應用)類加載器(System\App) --加載位置 :classpath 中
4、自定義加載器(必須繼承 ClassLoader)

  1. 類什么時候被初始化?

1)創建類的實例,也就是 new 一個對象
2)訪問某個類或接口的靜態變量,或者對該靜態變量賦值
3)調用類的靜態方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一個類的子類(會首先初始化子類的父類)
6)JVM 啟動時標明的啟動類,即文件名和類名相同的那個類

只有這 6 中情況才會導致類的類的初始化。

類的初始化步驟:

1)如果這個類還沒有被加載和鏈接,那先進行加載和鏈接
2)假如這個類存在直接父類,并且這個類還沒有被初始化(注意:在一個類加載器中,類只能初始化一 次),那就初始化直接的父類(不適用于接口)
3)加入類中存在初始化語句(如 static 變量和 static 塊),那就依次執行這些初始化語句。

  1. Java 類加載體系之 ClassLoader 雙親委托機制
    java 是一種類型安全的語言,它有四類稱為安全沙箱機制的安全機制來保證語言的安全性,這四類安全 沙箱分別是:
  1. 類加載體系
  2. .class 文件檢驗器
  3. 內置于 Java 虛擬機(及語言)的安全特性
  4. 安全管理器及 Java API

主要講解類的加載體系:
java 程序中的 .java 文件編譯完會生成 .class 文件,而 .class 文件就是通過被稱為類加載器的 ClassLoader加載的,而 ClassLoder 在加載過程中會使用“雙親委派機制”來加載 .class 文件,先上圖:

屏幕快照 2019-02-13 下午3.08.22.png

BootStrapClassLoader:啟動類加載器,一般用本地代碼實現,負責加載 JVM 基礎核心類庫(rt.jar);該 ClassLoader 是 jvm 在啟動時創建的,用于加 載 $JAVA_HOME$/jre/lib 下面的類庫(或者通過參數-Xbootclasspath 指定)。由于啟動類加載器涉及到虛擬機 本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不能直接通過引用進行操作。

ExtClassLoader:擴展類加載器,從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,它的父加載器是 Bootstrap; 該 ClassLoader 是在 sun.misc.Launcher 里作為一個內部類 ExtClassLoader 定義的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 會加載 $JAVA_HOME/jre/lib/ext 下的類庫(或者通過參數-Djava.ext.dirs 指定)。

AppClassLoader:應用程序類加載器,該 ClassLoader 同樣是在 sun.misc.Launcher 里作為一個內部類
AppClassLoader 定義的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader 會加載 java 環境變量 CLASSPATH 所指定的路徑下的類庫,而 CLASSPATH 所指定的路徑可以通過 System.getProperty("java.class.path")獲取;當然,該變量也可以覆蓋,可以使用參數-cp,例如:java -cp 路 徑 (可以指定要執行的 class 目錄)。

CustomClassLoader:自定義類加載器,該 ClassLoader 是指我們自定義的 ClassLoader,比如 tomcat 的 StandardClassLoader 屬于這一類;當然,大部分情況下使用 AppClassLoader 就足夠了。

前面談到了 ClassLoader 的幾類加載器,而 ClassLoader 使用雙親委派機制來加載 class 文件的。ClassLoader 的雙親委派機制是這樣的(這里先忽略掉自定義類加載器 CustomClassLoader):

1)當 AppClassLoader 加載一個 class 時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父 類加載器 ExtClassLoader 去完成。
2)當 ExtClassLoader 加載一個 class 時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。
3)如果 BootStrapClassLoader 加載失敗(例如在$JAVA_HOME$/jre/lib 里未查找到該 class),會使用 ExtClassLoader 來嘗試加載;
4)若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,如果 AppClassLoader 也加載失敗, 則會報出異常 ClassNotFoundException。

下面貼下 ClassLoader 的 loadClass(String name, boolean resolve)的源碼:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded,首先找緩存是否有class
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {//沒有判斷有沒有父類
                    if (parent != null) {//有的話,用父類遞歸獲取 class
                        c = parent.loadClass(name, false);
                    } else {//沒有父類。通過這個方法來加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order, 如果還是沒有找到,調用findClass(name)去找這個類
                    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;
        }
    }

代碼很明朗:首先找緩存(findLoadedClass),沒有的話就判斷有沒有 parent,有的話就用 parent 來遞歸 的 loadClass,然而 ExtClassLoader 并沒有設置 parent,則會通過 findBootstrapClassOrNull 來加載 class,而 findBootstrapClassOrNull 則會通過 JNI 方法”private native Class findBootstrapClass(String name)“來使用 BootStrapClassLoader 來加載 class。
然后如果 parent 未找到 class,則會調用 findClass 來加載 class,findClass 是一個 protected 的空方法, 可以覆蓋它以便自定義 class 加載過程。
另外,雖然 ClassLoader 加載類是使用 loadClass 方法,但是鼓勵用 ClassLoader 的子類重寫 findClass(String),而不是重寫 loadClass,這樣就不會覆蓋了類加載默認的雙親委派機制。

雙親委派托機制為什么安全
舉個例子,ClassLoader 加載的 class 文件來源很多,比如編譯器編譯生成的 class、或者網絡下載的字節碼。
而一些來源的 class 文件是不可靠的,比如我可以自定義一個 java.lang.Integer 類來覆蓋 jdk 中默認的 Integer 類,例如下面這樣:

public class Integer {
    public Integer(int value) {
        System.exit(0);
    }
}

初始化這個 Integer 的構造器是會退出 JVM,破壞應用程序的正常進行,如果使用雙親委派機制的話該 Integer 類永遠不會被調用,以為委托 BootStrapClassLoader 加載后會加載 JDK 中的 Integer 類而不會加載自定義 的這個,可以看下下面這測試個用例:

public static void main(String... args) {
        Integer i = new Integer(1);
        System.err.println(i);
    }

執行時 JVM 并未在 new Integer(1)時退出,說明未使用自定義的 Integer,于是就保證了安全性。

描述一下 JVM 加載 class
JVM 中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java 中的類加載器是一個重要的 Java 運 行時系統組件,它負責在運行時查找和裝入類文件中的類。

由于 Java 的跨平臺性,經過編譯的 Java 源程序并不是一個可執行程序,而是一個或多個類文件。當 Java 程序 需要使用某個類時,JVM 會確保這個類已經被加載、連接(驗證、準備和解析)和初始化。類的加載是指把類的.class 文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class 文件,然后產生與所加載類對應的 Class 對象。加 載完成后,Class 對象還不完整,所以此時的類還不可用。當類被加載后就進入連接階段,這一階段包括驗證、準備 (為靜態變量分配內存并設置默認的初始值)和解析(將符號引用替換為直接引用)三個步驟。最后 JVM 對類進行 初始化,包括:
如果類存在直接的父類并且這個類還沒有被初始化,那么就先初始化父類;
如果類中存在初始化語句,就依次執行這些初始化語句。類的加載是由類加載器完成的,類加載器包括:根加載器 (BootStrap)、擴展加載器(Extension)、系統加載器(System)和用戶自定義類加載器(java.lang.ClassLoader 的子類)。
從 Java 2(JDK 1.2)開始,類加載過程采取了父親委托機制(PDM)。PDM 更好的保證了 Java 平臺的安全 性,在該機制中,JVM 自帶的 Bootstrap 是根加載器,其他的加載器都有且僅有一個父類加載器。類的加載首先請求 父類加載器加載,父類加載器無能為力時才由其子類加載器自行加載。JVM 不會向 Java 程序提供對 Bootstrap 的引 用。

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

推薦閱讀更多精彩內容