【JVM第2課】類載入子系統(類載入器、雙親委派)

救苦救难韩天尊發表於2024-10-29

類載入系統載入類時分為三個步驟,載入、連結、初始化,下面展開介紹。

類載入子系統結構圖:

1 類載入器

JVM 使用類載入器載入 class 檔案,類載入器可分為引導類載入器自定義類載入器兩種。

引導類載入器(Bootstrap ClassLoader),有時也被稱作啟動類載入器或者零類載入器(Null ClassLoader),是 Java 虛擬機器中最基礎的類載入器之一。它的主要職責是載入 Java 核心類庫。

自定義類載入器需要繼承自 ClassLoader 類,JDK 預設提供了一些。比較重要的有兩個,擴充類載入器(ExtClassLoader)應用類載入器(AppClassLoader)

下面展開說說這三個載入器的作用、區別以及聯絡。先看一張圖:

1.1 引導類載入器(BootStrapClassLoader)

特點:

  1. 內部實現:引導類載入器並不是透過 Java 程式碼實現的,而是用 C++ 或者其他本地語言編寫的,並且是 JVM 的一部分。
  2. 載入路徑:引導類載入器通常從 $JAVA_HOME/jre/lib/ 或類似的位置載入 Java 核心類。
  3. 無父類載入器:引導類載入器沒有顯式的父類載入器,這是因為它的設計目的是為了載入 Java 最基礎的類庫,而這些類庫是任何其他類載入器工作的前提。因此,它不需要依賴於任何其他類載入器。
  4. 不可見性:引導類載入器並不是對所有 Java 應用程式都可見的,因為它是 JVM 的一部分,而不是標準的 Java 類載入器層次結構的一部分。
  5. 優先順序:引導類載入器通常是整個類載入過程的第一步,當 Java 應用程式啟動時,它會首先載入必要的核心類庫,然後才允許後續的類載入器(如擴充套件類載入器和應用類載入器)開始工作。

1.2 擴充類載入器(ExtClassLoader

特點:

  1. 內部實現ExtClassLoader是在sun.misc.Launcher類裡的靜態內部類,繼承自 ClassLoader 類,重寫 loadClass() 方法。
  2. 載入路徑ExtClassLoader 主要負責載入位於 $JAVA_HOME/jre/lib/ext 目錄下的擴充套件類庫。
  3. 委託模型ExtClassLoader 遵循 Java 類載入器的委託模型。當它收到一個類載入請求時,它首先會嘗試使用其父類載入器(即 Bootstrap ClassLoader)來載入這個類。如果父類載入器無法載入,則 ExtClassLoader 會嘗試自己載入。
  4. 優先順序ExtClassLoader 位於 BootstrapClassLoader 之後,但在 AppClassLoader之前。這意味著它繼承了 Bootstrap ClassLoader 的特性,並且它載入的類對 Application ClassLoader 可見。

1.3 應用類載入器(AppClassLoader)

特點:

  1. 內部實現AppClassLoader也是在sun.misc.Launcher類裡的靜態內部類,繼承自 ClassLoader 類,重寫 loadClass() 方法。
  2. 載入路徑AppClassLoader主要負責的目錄是當前應用程式的 classpath 所指定的路徑,也就是說我們自己寫的類預設都是透過AppClassLoader載入的。我們在IDEA裡執行程式碼時,仔細觀察控制檯可以發現第一行透過-classpath指定了當前應用程式的class檔案的目錄
  3. 委託模型AppClassLoader 遵循 Java 類載入器的委託模型。當它收到一個類載入請求時,它首先會嘗試使用其父類載入器 ExtClassLoader來載入這個類。如果父類載入器無法載入,則 AppClassLoader 會嘗試自己載入。
  4. 優先順序AppClassLoader位於ExtClassLoader 之後。

除了這些特點外,AppClassLoader還有一些別的用途:

  1. 載入第三方 Jar 包:當應用程式依賴於第三方庫時,這些庫通常會被打包成 JAR 包,並放置在類路徑中。AppClassLoader 會載入這些 JAR 包中的類。
  2. 動態載入類:在一些需要動態載入類的場景中,如 Spring Boot 應用程式,AppClassLoader 可以用於動態載入和解除安裝類。

1.4 雙親委派

原因一:前面我們介紹了引導類載入器、擴充類載入器、應用類載入器分別負責不同的路徑下的class檔案,但是並不是完全不相交的,比如-classpath除了指定當前應用程式的class檔案目錄外,也會指定$JAVA_HOME/jre/lib/目錄下的某些 jar 包,所以要避免重複載入某些類。

原因二:如果我們的程式被駭客攻擊了,比如駭客自己建立了一個java.lang的包,裡面建立了一個名為String的類,把這個包和類植入我們正在執行的專案裡,如果他的這個類被載入了,那我們專案裡的String就會被篡改。

為了避免以上兩種原因,我們要保證類只載入一次,並且保證越靠近 JVM 的類載入器優先順序越高。這就是雙親委派乾的事情!!!

原理:

引導類載入器、擴充類載入器、應用類載入器這三者之前有個關係,但又不是父子類關係,而是應用類載入器有個parent屬性是擴充類載入器的物件。擴充類載入器的parent為空,所以會呼叫引導類載入器。我們觀察ClassLoaderloaderClass()方法可以得出類的載入過程:

loadClass()方法

雙親委派機制

簡單來說就是,

透過AppClassLoader載入class時會先用ExtClassLoader去載入這個類;

透過ExtClassLoader載入class時會先用BootStrapClassLoader去載入這個類;

好處:

  • 避免類被重複載入。
  • 防止JVM核心類被篡改。

2 連結

class 載入完後會進行連結,分為三步:驗證、準備、解析。

2.1 驗證

第一步是驗證 class 檔案是否正確,比如驗證格式。

2.2 準備

對 static 修飾的屬性賦予一個預設值,但這一步不會賦初始值。

舉個例子,class 裡定義了一個static int a = 1,準備階段會把 a 賦值為 0,在初始化階段 a 才會 = 1。

2.3 解析

將符號引用解析為直接引用。什麼意思呢?

首先我們需要知道類被載入後是放到方法區的,每個類都是一個 Klass 物件(也可稱為 Klass 結構)。

一般情況下我們都會在一個A類裡使用到別的B類,使用方式是B類的全限定名,就只是一個字串。但是JVM 實際在執行的時候需要從方法區中找到B類的 Klass 物件,解析的作用就是把這個名稱字串替換為實際的 Klass 物件記憶體地址。“符號引用”就是名稱字串、“直接引用”就是 Klass 物件記憶體地址。

3 初始化

初始化是類載入子系統的最後一個階段,也是最為關鍵的階段之一。下面詳細介紹初始化階段的內容及其重要性。

3.1 定義

初始化階段是類載入過程中的最後一個階段,它負責執行類構造器 <clinit> 方法,並初始化類的靜態變數。

3.2 主要任務

初始化階段的主要任務包括:

  • 執行類構造器 <clinit> 方法<clinit> 方法是一個特殊的靜態構造器,它負責對類進行初始化。每個類都有一個 <clinit> 方法,該方法在類第一次被初始化時由 JVM 自動生成並執行。
  • 初始化類變數:類中的靜態變數(即類變數)在 <clinit> 方法中被賦值。

3.3 <clinit> 方法的特點

  • 靜態塊:在類定義中,靜態程式碼塊會被編譯器轉化為 <clinit> 方法中的語句。
  • 順序執行:如果一個類有多個靜態程式碼塊,它們將按照在原始碼中出現的順序依次執行。
  • 執行緒安全<clinit> 方法是執行緒安全的,這意味著即使有多個執行緒同時初始化同一個類,也不會發生衝突。

3.4 示例程式碼

下面是一個簡單的示例,展示類的初始化過程:

    public class InitializationExample {
        static {
            System.out.println("執行靜態初始化塊。");
        }

        static int staticVar = initializeStaticVar();

        private static int initializeStaticVar() {
            System.out.println("初始化靜態變數。");
            return 10;
        }

        public static void main(String[] args) {
            System.out.println("靜態變數初始化為: " + staticVar);
        }
    }

輸出如下:

    執行靜態初始化塊。
    初始化靜態變數。
    靜態變數初始化為: 10

3.5 初始化時機

類的初始化通常在以下幾種情況下觸發:

  • 首次建立類的例項:當第一次建立類的例項時,JVM 會初始化該類。
  • 呼叫類的靜態方法:當第一次呼叫類的靜態方法時,JVM 會初始化該類。
  • 引用類的靜態欄位:當第一次引用類的靜態欄位時,JVM 會初始化該類。
  • 反射性引用:當透過 java.lang.Classjava.lang.reflect 包中的方法來引用類時,如果這些方法會導致類的初始化,那麼 JVM 會初始化該類。
  • 初始化子類時:當初始化一個類的子類時,如果父類還沒有被初始化,那麼 JVM 會首先初始化父類。

3.6 初始化順序

類的初始化順序遵循一定的規則:

  • 如果類 A 引用了類 B 的靜態欄位或呼叫了類 B 的靜態方法,那麼類 B 必須先於類 A 被初始化。
  • 如果類 A 繼承自類 B,那麼類 B 必須先於類 A 被初始化。

相關文章