天天看點

簡述Java 虛拟機加載 Java 類的三步

文章目錄

      • 加載
      • 連結
      • 初始化
      • 小結

關于 Java 虛拟機中的類加載,從 class 檔案到記憶體中的類,按先後順序需要經過加載、連結以及初始化三大步驟。其中,連結過程中同樣需要驗證;而記憶體中的類沒有經過初始化也不能使用。那麼,是不是所有的了 Java 類都需要經過這幾步呢?

Java 語言的類型可以分為基本類型(primitive types) 和引用類型(reference types)。而應用類型有可以細分為四類:類、接口、數組類和泛型參數。由于泛型參數會在編譯過程中被擦出,是以 Java 虛拟機實際上隻有前三種。在類、接口和數組類中,數組類是由 Java 虛拟機直接生成的,其他兩種則有對應的位元組流。

位元組流:常見的形式如由 Java 編譯器生成的 class 檔案。除此之外,我們也可以在程式内部直接生成,或者從網絡中擷取(如網頁中内嵌的小程式 Java applet) 位元組流。這些不同的位元組流,都會被加載到 Java 虛拟機中,成為類或接口。(以下用"類"來統稱)

無論是直接生成的數組類還是加載類,Java 虛拟機都需要對其進行連結和初始化,以下詳述

加載

加載,是指查找位元組流,并且據此建立類的過程。對于數組類它并沒有對應的位元組流,而是由 Java 虛拟機 直接生成的。對于其他的類,Java 虛拟機則需要借助類加載器完成查找位元組流的過程。

以村裡的小方蓋房子為例,他首先得找個建築師為他設計一個房型,比如“三室、一廳、四衛”。而這裡的房型就相當于類,而建築師就相當于類加載器。

村裡建築師很多,但有以為共同的老師父,叫啟動類加載器(bootstrap class loader) 。啟動類加載器是由 C++ 實作的,沒有對應的 Java 對象,是以 Java 隻能用null來指代。就像老師父不喜歡小方這種小人物,是以誰也沒有他的聯系方式。

除了啟動類加載器之外,其他的類加載器都是 java.lang.ClassLoader 的子類,是以由對應的 Java 對象。這些類加載器需要先由另一個類加載器,比如啟動類加載器,加載至虛拟機中,才能執行類加載。好比村裡的小建築師街道單子不能直接上手敢,得先師父過目。這個過程就叫“雙親委派模型”。即:每一個類加載器在接受到加載請求時,它會先将請求轉發給父類加載器,直到父類加載器沒有找到所請求的類的情況下,該類加載器才會嘗試加載。

簡述Java 虛拟機加載 Java 類的三步

Java 9 引入了子產品系統,并且略微更改了上述的類加載器。擴充類加載器被改名為平台類加載器(platform class loader)。Java SE 中除了少數幾個關鍵子產品,比如說 java.base 是由啟動類加載器加載之外,其他的子產品均由平台類加載器所加載。

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

連結

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

驗證階段的目的,在于確定被加載類能夠滿足 Java 虛拟機的限制條件。好比小方将設計好的房型要得到市政部門稽核通過才可開始建造工作。

準備階段的目的,則是為被加載類的靜态字段配置設定記憶體。Java 代碼中對靜态字段的具體初始化,則會在稍後的初始化階段中進行。好比建好了毛胚房。雖然結構完整,沒有裝修還不能住人。除了配置設定記憶體,部分 Java 虛拟機還會再此階段構造其他和類層次相關的資料結構,比如說實作虛方法的動态綁定方法表。在 class 檔案被加載至 Java 虛拟機前,這個類無法知道其他類及其方法、字段所對應的具體位址,甚至不知道自己方法、字段的位址。是以,每當需要引用這些成員時,Java 編譯器會生成一個符号引用。在運作階段,這個符号引用一般能夠無歧義定位到具體目标。

舉例來說,對于一個方法調用,編譯器會生成一個包含目标方法所在類的名字、目标方法的名字、接收參數類型以及傳回類型的符号引用,來指代所要調用的方法。

解析階段的目的,正是将這些符号引用解析成為實際引用。如果符号引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析将觸發這個類的加載(但未必觸發這個類的連結以及初始化) 。好比放在蓋房子例子中,符号引用就是"小方的房子"。實際引用就像實際的通訊位址,如果想要與小方通信,則要啟動蓋房子的過程。

Java 虛拟機規範并沒有要求在連結的過程中完成解析。他僅規定了:如果某些位元組碼使用了符号引用,那麼在執行這些位元組碼之前,需要完成對這些符号引用的解析。

初始化

在 Java 代碼中,如果要初始化一個靜态字段,我們可以在聲明時直接指派,也可以在靜态代碼塊中對其指派。

如果直接指派的靜态字段被 final 所修飾,并且它的類型是基本類型或字元串時,那麼該字段便會被 Java 編譯器标記成常量值(ConstantValue),其初始化直接由 Java 虛拟機完成。除此之外的直接指派操作,以及所有靜态代碼塊中的代碼,則會被 Java 編譯器置于同一方法中,并把它命名為 。

類加載的最後一步是初始化,便是為标記為常量值的字段指派,以及執行 方法的過程。Java 虛拟機會通過加鎖來確定類的 方法僅被執行一次。

隻有當初始化完成之後,類才正式成為可執行的狀态,那麼類的初始化何時會被觸發? JVM 規範列舉了下述多種觸發情況:

  1. 當虛拟機啟動時,初始化使用者指定的主類;
  2. 當遇到用以建立目标類執行個體的 new 指令時,初始化 new 指令的目标類;
  3. 當遇到調用靜态方法的指令時,初始化該靜态方法所在類;
  4. 當遇到通路靜态字段的指令時,初始化該靜态字段所在的類;
  5. 子類的初始化會觸發父類的初始化;
  6. 一個接口定義了 default 方法,直接實作或間接實作該接口的類的初始化,觸發接口初始化
  7. 使用反射 API 對某個類進行反射調用時,初始化這個類;
  8. 當初次調用 MethodHandle 執行個體時,初始化該 Methodhandle 指向的方法所在的類

小結

Java 虛拟機将位元組流轉化為 Java 類可分為 加載、連結、初始化三大步驟。

  • 加載是指查找位元組流,并且據此建立類的過程。加載需要借助類的加載器,在 Java 虛拟機中,類加載器使用了雙親委派模型,即接受到加載請求時,會先将請求轉發給父類加載器。
  • 連結,是指将建立成的類合成并至 Java 虛拟機中,使之能夠執行的過程。連結還分驗證、準備和解析三個階段。其中,解析階段是非必須的。
  • 初始化,則是為标記為常量值的字段指派,以及執行 方法的過程。類的初始化僅會被執行一次,這個特性被用來實作單例的延遲初始化。

簡單來說,虛拟機必須知道(加載)有這個類,才能建立這個類的數組(容器),但是這個類還沒有使用(沒有達到初始化的條件),是以不會被初始化。建立數組時并不是要使用這個類(隻是定義了方這個類的容器),是以不會被連結,當告訴虛拟機要使用這個,把類造好(連結),然後把 字元賦予變量(初始化)。