Java虛擬機是如何載入Java類的?

  • 2019 年 10 月 8 日
  • 筆記

Java 虛擬機中的類載入即從 class 文件到記憶體中的類,按先後順序需要經過載入、鏈接以及初始化三大步驟。

虛擬機的載入對象是什麼?

上文中說過Java中有兩種類型:基本類型和引用類型,而基本類型是由虛擬機預先定義好的,引用類型中的泛型參數又會在編譯過程中被擦除,所以載入的對象就剩下類、介面和數組類。

在類、介面和數組類中,數組類是由 Java 虛擬機直接生成的,其他兩種則有對應的位元組流。無論是直接生成的數組類,還是載入的類,Java 虛擬機都需要對其進行鏈接和初始化。接下來,就詳細介紹一下每個步驟具體都在幹些什麼。

虛擬機的載入流程是什麼?

1.載入

是指查找位元組流,並且據此創建類的過程。上面提過數組類是由Java虛擬機直接生成的,所以載入過程針對的是生成位元組流的類與介面。如何找到這些位元組流,則需要虛擬機藉助類載入器。

啟動類載入器是由 C++ 實現的,沒有對應的 Java 對象,因此在 Java 中只能用 null 來指代。在 Java 9 之前,啟動類載入器負責載入最為基礎、最為重要的類,比如存放在 JRE 的 lib 目錄下 jar 包中的類(以及由虛擬機參數 -Xbootclasspath 指定的類)。除了啟動類載入器之外,另外兩個重要的類載入器是擴展類載入器(extension class loader)和應用類載入器(application class loader),均由 Java 核心類庫提供。故除了啟動類載入器之外,其他的類載入器都是java.lang.ClassLoader 的子類,因此有對應的 Java 對象。

擴展類載入器的父類載入器是啟動類載入器。它負責載入相對次要、但又通用的類,比如存放在 JRE 的 lib/ext 目錄下 jar 包中的類(以及由系統變數 java.ext.dirs 指定的類)。

應用類載入器的父類載入器則是擴展類載入器。它負責載入應用程式路徑下的類。(這裡的應用程式路徑,便是指虛擬機參數 -cp/-classpath、系統變數 java.class.path 或環境變數 CLASSPATH 所指定的路徑。)默認情況下,應用程式中包含的類便是由應用類載入器載入的。

Java 9 引入了模組系統,並且略微更改了上述的類載入器1。擴展類載入器被改名為平台類載入器(platform class loader)。Java SE 中除了少數幾個關鍵模組,比如說 java.base 是由啟動類載入器載入之外,其他的模組均由平台類載入器所載入。當然還可以自定義類載入器哦。

除了載入功能之外,類載入器還提供了命名空間的作用,在 Java 虛擬機中,類的唯一性是由類載入器實例以及類的全名一同確定的。即便是同一串位元組流,經由不同的類載入器載入,也會得到兩個不同的類。在大型應用中,我們往往藉助這一特性,來運行同一個類的不同版本。

2.鏈接

是指將創建成的類合併至 Java 虛擬機中,使之能夠執行的過程。它可分為驗證、準備以及解析三個階段。

  • 驗證階段:確保被載入類能夠滿足 Java 虛擬機的約束條件。
  • 準備階段:為被載入類的靜態欄位分配記憶體,構造其他跟類層次相關的數據結構。
  • 解析階段:將符號引用解析成為實際引用(Java 虛擬機規範並沒有要求在鏈接過程中完成解析。它僅規定了:如果某些位元組碼使用了符號引用,那麼在執行這些位元組碼之前,需要完成對這些符號引用的解析)。

符號引用則是在 class 文件被載入至 Java 虛擬機之前,類無法知道其他類及其方法、欄位所對應的具體地址,甚至不知道自己方法、欄位的地址。每當需要引用這些成員時,Java 編譯器會生成一個符號引用。在運行階段,這個符號引用一般都能夠無歧義地定位到具體目標上。

3.初始化

初始化即給常量賦值以及執行 < clinit > 方法的過程,完成之後,類才正式成為可執行的狀態。

類初始化觸發條件

  • 當虛擬機啟動時,初始化用戶指定的主類;
  • 當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;
  • 當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;
  • 當遇到訪問靜態欄位的指令時,初始化該靜態欄位所在的類;
  • 子類的初始化會觸發父類的初始化;
  • 如果一個介面定義了 default 方法,那麼直接實現或者間接實現該介面的類的初始化,會觸發該介面的初始化;
  • 使用反射 API 對某個類進行反射調用時,初始化這個類;
  • 當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

總結

虛擬機載入Java類是Java 虛擬機將位元組流轉化為 Java 類的過程。這個過程可分為載入、鏈接以及初始化三大步驟。

  • 載入:是指查找位元組流,並且據此創建類的過程。載入需要藉助類載入器,在 Java 虛擬機中,類載入器使用了雙親委派模型,即接收到載入請求時,會先將請求轉發給父類載入器。
  • 鏈接:是指將創建成的類合併至 Java 虛擬機中,使之能夠執行的過程。鏈接還分驗證、準備和解析三個階段。其中,解析階段為非必須的。
  • 初始化:是為標記為常量值的欄位賦值,以及執行 < clinit > 方法的過程。類的初始化僅會被執行一次,這個特性被用來實現單例的延遲初始化。