1. 程式人生 > 實用技巧 >圖解類載入器和雙親委派機制,一看就懂

圖解類載入器和雙親委派機制,一看就懂

聽說微信搜尋《Java魚仔》會變更強哦!

本文收錄於JavaStarter,裡面有我完整的Java系列文章,學習或面試都可以看看哦

(一)概述

我們都知道Java程式碼會被編譯成class檔案,在class檔案中描述了該類的各種資訊,class類最終需要被載入到虛擬機器中才能執行和使用。

虛擬機器把Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成虛擬機器可以直接使用的Java型別,這就是虛擬機器的類載入機制。

(二)類載入的過程

一個類從被載入到卸載出記憶體,一共包含下面七個階段:

載入、驗證、準備、解析、初始化、使用、解除安裝載入的來源有以下部分:

1、本地磁碟

2、網路下載的.class檔案

3、war,jar下載入.class檔案

4、從專門的資料庫中讀取.class檔案(少見)

5、將java原始檔動態編譯成class檔案,典型的就是動態代理,通過執行時生成class檔案

載入的過程是通過類載入器實現的。有關類載入的其他過程我會在下一章中介紹。

(三)類載入器的分類

類載入器分為系統級別和使用者級別:

系統級別的類載入器有:

1、啟動類載入器(底層使用C++實現)

2、擴充套件類載入器(底層使用java實現,是ClassLoader的子類)

3、應用程式類載入器(底層使用java實現,是ClassLoader的子類)

使用者級別的類載入器我們統一稱為自定義類載入器。

3.1 啟動類載入器

首先我們來看看啟動類載入器載入了哪些類,啟動類載入器負責載入sun.boot.class.path:

public static void bootClassLoaderLoadingPath(){
    //獲取啟動列載入器載入的目錄
    String bootStrapLoadingPath=System.getProperty("sun.boot.class.path");
    //把載入的目錄轉為集合
    List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
    for (String bootPath:bootLoadingPathList){
        System.out.println("啟動類載入器載入的目錄:"+bootPath);
    }
}

通過上面的程式碼我們可以獲取到啟動類載入器所載入的類:

3.2 拓展類載入器

擴充套件類載入器載入負責載入java.ext.dirs,我們同樣寫一段程式碼去載入它:

public static void extClassLoaderLoadingPath(){
    //獲取啟動列載入器載入的目錄
    String bootStrapLoadingPath=System.getProperty("java.ext.dirs");
    //把載入的目錄轉為集合
    List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
    for (String bootPath:bootLoadingPathList){
        System.out.println("拓展類載入器載入的目錄:"+bootPath);
    }
}

可以看到,除了載入了JDK目錄下的ext外,還載入了Sun目錄下的ext

3.3 應用程式類載入器

最後是應用類載入器,它負責載入java.class.path:

public static void appClassLoaderLoadingPath(){
    //獲取啟動列載入器載入的目錄
    String bootStrapLoadingPath=System.getProperty("java.class.path");
    //把載入的目錄轉為集合
    List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";"));
    for (String bootPath:bootLoadingPathList){
        System.out.println("應用程式類載入器載入的目錄:"+bootPath);
    }
}

它負責載入工程目錄下classpath下的class以及jar包。

(四)雙親委派模型

所謂雙親委派模型,就是指一個類接收到類載入請求後,會把這個請求依次傳遞給父類載入器(如果還有的話),如果頂層的父類載入器可以載入,就成功返回,如果無法載入,再依次給子載入器去載入。我們先通過程式碼來看一下類載入器的層級結構:

public class ClassLoaderPath {
    public static void main(String[] args) {
        System.out.println(ClassLoaderPath.class.getClassLoader());
        System.out.println(ClassLoaderPath.class.getClassLoader().getParent());
        System.out.println(ClassLoaderPath.class.getClassLoader().getParent().getParent());
    }
}

編寫一個類,依次輸出這個類的類載入器,父類載入器,父類的父類載入器

可以看到首先是應用程式類載入器,它的父類是擴充套件類載入器,擴充套件類載入器的父類輸出了一個null,這個null會去呼叫啟動類載入器。如果你不信,我們看原始碼:ClassLoader類

接著從父類載入器往下呼叫findClass,如果可以載入,就直接返回class,如果不能載入,就依次向下。如果到了自定義載入器還是無法被載入,就會丟擲ClassNotFound異常。

我畫了一個流程圖來展示雙親委派模型的全過程:

雙親委派模型保證了Java程式的穩定執行,可以避免類的重複載入,也保證了 Java 的核心 API 不被篡改。

(五)破壞雙親委派

雙親委派模型並不是絕對的,spi機制就可以打破雙親委派模型。

首先我們需要了解什麼是spi,spi(Service Provider Interface)是一種服務發現機制,Java在核心庫中定義了許多介面,並且針對這些介面給出呼叫邏輯,但是並未給出具體的實現。開發者要做的就是定製一個實現類,在 META-INF/services 中註冊實現類資訊,以供核心類庫使用。最典型的就是JDBC。

Java提供了一個Driver介面用於驅動各個廠商的資料庫連線,Driver類位於JAVA_HOME中jre/lib/rt.jar中,應該由Bootstrap類載入器進行載入。根據類載入機制,當被載入的類引用了另外一個類的時候,虛擬機器就會使用載入該類的類載入器載入被引用的類,因此如果其他資料庫廠商定製了Driver的實現類之後,按理說也得把這個實現類放到啟動類載入器載入的目錄下,這顯然是很不合理的。

於是Java提供了spi機制,即使Driver由啟動類載入器去載入,但是他可以讓執行緒上下文載入器(Thread Context ClassLoader)去請求子類載入器去完成載入,預設是應用程式類載入器。但是這確實破壞了類載入機制。