JVM類加載過程與雙親委派

Class 文件需要加載到虛擬機中之後才能運行和使用,那麼虛擬機是如何加載這些 Class 文件呢?

系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分爲三步:驗證->準備->解析

類加載過程

加載

類加載過程的第一步,主要完成下面3件事情:

  1. 通過全類名獲取定義此類的二進制字節流

  2. 將字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構

  3. 在內存中生成一個代表該類的 Class 對象,作爲方法區這些數據的訪問入口

虛擬機規範多上面這3點並不具體,因此是非常靈活的。比如:"通過全類名獲取定義此類的二進制字節流" 並沒有指明具體從哪裏獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日後出現的JAR、EAR、WAR格式的基礎)、其他文件生成(典型應用就是JSP)等等。

一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass() 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。

類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在後面的文章中單獨介紹到。

加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。

驗證

驗證階段示意圖

準備

準備階段是正式爲  類變量  分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:

  1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。

  2. 這裏所設置的初始值"通常情況"下是數據類型默認的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,那麼 value 變量在準備階段的初始值就是 0 而不是111(初始化階段纔會複製)。特殊情況:比如給 value 變量加上了 fianl 關鍵字public static final int value=111 ,那麼準備階段 value 的值就被複製爲 111。

基本數據類型的零值:

基本數據類型的零值

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。

符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機爲每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變爲目標方法在類中方法表的位置,從而使得方法可以被調用。

綜上,解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。

初始化

初始化是類加載的最後一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 <clinit> ()方法的過程。

對於<clinit>() 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因爲 <clinit>() 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,並且這種死鎖很難被發現。

對於初始化階段,虛擬機嚴格規範了有且只有5中情況下,必須對類進行初始化:

1、當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。

2、使用 java.lang.reflect 包的方法對類進行反射調用時 ,如果類沒初始化,需要觸發其初始化。

3、初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。

4、當虛擬機啓動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。

5、當使用 JDK1.7 的動態動態語言時,如果一個 MethodHandle 實例的最後解析結構爲 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,並且這個句柄沒有初始化,則需要先觸發器初始化。

 

什麼是雙親委派

從 Java 虛擬機的角度來說,只存在兩種不同的類加載器。

Bootstrap ClassLoader
java.lang.ClassLoader

從 Java 開發人員的角度來看,大部分 Java 程序一般會使用到以下三種系統提供的類加載器:

  1. 啓動類加載器 Bootstrap ClassLoader 。負責加載 <JAVA_HOME>\lib 目錄中並且能被虛擬機識別的類庫到虛擬機內存中,如果名稱不符合的類庫即使放在 lib 目錄中也不會被加載。該類加載器無法被 Java 程序直接引用
  2. 擴展類加載器 Extension ClassLoader 。該加載器主要是負責加載 <JAVA_HOME>\lib\ext ,該加載器可以被開發者直接使用
  3. 應用程序類加載器 Application ClassLoader 。該類加載器也稱爲系統類加載器,它負責加載用戶類路徑 CLASS_PATH 上所指定的類庫,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器

下圖展示了類加載器之間的這種層次關係。這種模型被稱爲類加載器的 雙親委派模型

類加載機制與雙親委派

 

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求都最終應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子類加載器纔會嘗試自己去加載。

 

爲什麼這麼設計?

使用 雙親委派模型 來組織類加載器之間的關係,有一個顯而易見的好處就是 Java 類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類 java.lang.Object ,它存放在 rt.jar 之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類自行去加載的話,如果用戶自己編寫了一個 java.lang.Object 的類,並放在程序的 CLASS_PATH 中,那系統中將會出現多個不同的 Object 類,Java 體系同最基礎的行爲也就無法保證,應用程序也會變得一片混亂。

 

參考:

https://www.toutiao.com/i6817628081272914440/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章