類載入系統載入類時分為三個步驟,載入、連結、初始化,下面展開介紹。
類載入子系統結構圖:
1 類載入器
JVM 使用類載入器載入 class 檔案,類載入器可分為引導類載入器和自定義類載入器兩種。
引導類載入器(Bootstrap ClassLoader),有時也被稱作啟動類載入器或者零類載入器(Null ClassLoader),是 Java 虛擬機器中最基礎的類載入器之一。它的主要職責是載入 Java 核心類庫。
自定義類載入器需要繼承自 ClassLoader 類,JDK 預設提供了一些。比較重要的有兩個,擴充類載入器(ExtClassLoader) 和應用類載入器(AppClassLoader) 。
下面展開說說這三個載入器的作用、區別以及聯絡。先看一張圖:
1.1 引導類載入器(BootStrapClassLoader)
特點:
- 內部實現:引導類載入器並不是透過 Java 程式碼實現的,而是用 C++ 或者其他本地語言編寫的,並且是 JVM 的一部分。
- 載入路徑:引導類載入器通常從
$JAVA_HOME/jre/lib/
或類似的位置載入 Java 核心類。 - 無父類載入器:引導類載入器沒有顯式的父類載入器,這是因為它的設計目的是為了載入 Java 最基礎的類庫,而這些類庫是任何其他類載入器工作的前提。因此,它不需要依賴於任何其他類載入器。
- 不可見性:引導類載入器並不是對所有 Java 應用程式都可見的,因為它是 JVM 的一部分,而不是標準的 Java 類載入器層次結構的一部分。
- 優先順序:引導類載入器通常是整個類載入過程的第一步,當 Java 應用程式啟動時,它會首先載入必要的核心類庫,然後才允許後續的類載入器(如擴充套件類載入器和應用類載入器)開始工作。
1.2 擴充類載入器(ExtClassLoader
)
特點:
- 內部實現:
ExtClassLoader
是在sun.misc.Launcher
類裡的靜態內部類,繼承自ClassLoader
類,重寫loadClass()
方法。 - 載入路徑:
ExtClassLoader
主要負責載入位於$JAVA_HOME/jre/lib/ext
目錄下的擴充套件類庫。 - 委託模型:
ExtClassLoader
遵循 Java 類載入器的委託模型。當它收到一個類載入請求時,它首先會嘗試使用其父類載入器(即 Bootstrap ClassLoader)來載入這個類。如果父類載入器無法載入,則ExtClassLoader
會嘗試自己載入。 - 優先順序:
ExtClassLoader
位於BootstrapClassLoader
之後,但在AppClassLoader
之前。這意味著它繼承了Bootstrap ClassLoader
的特性,並且它載入的類對Application ClassLoader
可見。
1.3 應用類載入器(AppClassLoader)
特點:
- 內部實現:
AppClassLoader
也是在sun.misc.Launcher
類裡的靜態內部類,繼承自ClassLoader
類,重寫loadClass()
方法。 - 載入路徑:
AppClassLoader
主要負責的目錄是當前應用程式的classpath
所指定的路徑,也就是說我們自己寫的類預設都是透過AppClassLoader
載入的。我們在IDEA裡執行程式碼時,仔細觀察控制檯可以發現第一行透過-classpath
指定了當前應用程式的class
檔案的目錄 - 委託模型:
AppClassLoader
遵循 Java 類載入器的委託模型。當它收到一個類載入請求時,它首先會嘗試使用其父類載入器ExtClassLoader
來載入這個類。如果父類載入器無法載入,則AppClassLoader
會嘗試自己載入。 - 優先順序:
AppClassLoader
位於ExtClassLoader
之後。
除了這些特點外,AppClassLoader
還有一些別的用途:
- 載入第三方 Jar 包:當應用程式依賴於第三方庫時,這些庫通常會被打包成 JAR 包,並放置在類路徑中。AppClassLoader 會載入這些 JAR 包中的類。
- 動態載入類:在一些需要動態載入類的場景中,如 Spring Boot 應用程式,AppClassLoader 可以用於動態載入和解除安裝類。
1.4 雙親委派
原因一:前面我們介紹了引導類載入器、擴充類載入器、應用類載入器分別負責不同的路徑下的class
檔案,但是並不是完全不相交的,比如-classpath
除了指定當前應用程式的class
檔案目錄外,也會指定$JAVA_HOME/jre/lib/
目錄下的某些 jar 包,所以要避免重複載入某些類。
原因二:如果我們的程式被駭客攻擊了,比如駭客自己建立了一個java.lang的包,裡面建立了一個名為String的類,把這個包和類植入我們正在執行的專案裡,如果他的這個類被載入了,那我們專案裡的String就會被篡改。
為了避免以上兩種原因,我們要保證類只載入一次,並且保證越靠近 JVM 的類載入器優先順序越高。這就是雙親委派乾的事情!!!
原理:
引導類載入器、擴充類載入器、應用類載入器這三者之前有個關係,但又不是父子類關係,而是應用類載入器有個parent屬性是擴充類載入器的物件。擴充類載入器的parent為空,所以會呼叫引導類載入器。我們觀察ClassLoader
的loaderClass()
方法可以得出類的載入過程:
簡單來說就是,
透過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.Class
或java.lang.reflect
包中的方法來引用類時,如果這些方法會導致類的初始化,那麼 JVM 會初始化該類。 - 初始化子類時:當初始化一個類的子類時,如果父類還沒有被初始化,那麼 JVM 會首先初始化父類。
3.6 初始化順序
類的初始化順序遵循一定的規則:
- 如果類 A 引用了類 B 的靜態欄位或呼叫了類 B 的靜態方法,那麼類 B 必須先於類 A 被初始化。
- 如果類 A 繼承自類 B,那麼類 B 必須先於類 A 被初始化。