JVM之類載入器、載入過程及雙親委派機制

農夫三拳有點疼~發表於2020-05-06

JVM 的生命週期

虛擬機器的啟動

Java 虛擬機器的啟動是通過引導類載入器(bootstrap class loader)建立一個初始類(initial class)來完成的,這個類是由虛擬機器的具體實現指定的。

虛擬機器的執行

  • 一個執行中的 Java 虛擬機器有著一個清晰的任務:執行 Java 程式。
  • 程式開始執行時他才執行,程式結束時他就停止。
  • 執行一個所謂的 Java 程式的時候,真真正正在執行的是一個叫做 Java 虛擬機器的程式。

虛擬機器的退出

有如下的幾種情況:

  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致 Java 虛擬機器程式終止
  • 某執行緒呼叫 Runtime 類或 System 類的 exit 方法,或 Runtime 類的 halt 方法,並且 Java 安全管理器也允許這次 exit 或 halt 操作。
  • 除此之外,JNI ( Java Native Interface) 規範描述了用 JNI Invocation API 來載入或解除安裝 Java 虛擬機器時,Java虛擬機器的退出情況。

類載入器子系統的作用

  • 類載入器子系統負責從檔案系統或者網路中載入 class 檔案,class檔案的開頭有特定的檔案標識(CA FE BA BE)
  • ClassLoader 只負責 class 檔案的載入,至於它是否可以執行,則由 Execution Engine 決定
  • 載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是 class 檔案中常量池部分的記憶體對映)

類載入器 ClassLoader 的作用

.class 檔案 → JVM → 最終成為後設資料模板,在這一過程中,ClassLoader 充當了運輸工具,扮演了一個快遞員的角色

類的載入過程

載入(Loading)

1、通過一個類的全限定類名獲取定義此類的二進位制位元組流

2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構

3、在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區裡這個類的各種資料的訪問入口

連結(Linking)

驗證

  • 確保 class 檔案的位元組流中包含資訊符合當前虛擬機器要求,保證被載入類的正確性,不會危害虛擬機器自身安全
  • 包括四種驗證方式:檔案格式驗證、後設資料驗證、位元組碼驗證、符號引用驗證

準備

  • 為類變數分配記憶體並且設定該類變數的預設初始值,即零值。
  • 這裡不包含用 final 修飾的靜態常量 ,因為 final 在編譯的時候就會分配了,準備階段會顯式初始化
  • 這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java 堆中。

解析

  • 將常量池內的符號引用轉換為直接引用的過程。
  • 事實上,解析操作往往會伴隨著 JVM 在執行完初始化之後再執行。
  • 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機器
    規範》的 Class 檔案格式中。直接引用就是直接指向目標的指標、相對偏移量或一個間接定位
    到目標的控制程式碼。
  • 解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對應常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT Methodref_info等。

初始化(Initialization)

  • 初始化階段就是執行類構造器方法 () 的過程。
  • 此方法不需定義,是 javac 編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。
  • 構造器方法中指令按語句在原始檔中出現的順序執行。
  • () 不同於類的構造器。(關聯: 構造器是虛擬機器視角下的 () )
  • 若該類具有父類,JVM 會保證子類的 () 執行前,父類的 () 已經執行完畢。
  • 虛擬機器必須保證一個類的 () 方法在多執行緒下被同步加鎖。

類載入器的分類

JVM 支援兩種型別的類載入器:引導類載入器(Bootstrap ClassLoader)和自定義類載入器(User-Defined ClassLoader)。

這裡說的自定義類載入器並不是指由開發人員自定義的類載入器,在 Java 虛擬機器規範中,將所有派生於抽象類ClassLoader 的所有類載入器都稱為自定義類載入器。所以,無論類載入器的型別如何劃分,在程式中,我們最常見的類載入器始終只有 3 個,如下圖所示:

這四者之間是包含的關係,不是上下層,也不是子父類繼承關係

啟動類載入器(引導類載入器,Bootstrap ClassLoader)

  • 這個類載入器使用 C/C++ 編寫,巢狀在 JVM 內部

  • 它用來載入 Java 核心類庫 ( JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path 路徑下的內容),用於提供 Java 自身需要的類

  • 並不繼承 ClassLoader,沒有父載入器

  • 載入擴充套件類和應用程式類載入器,並指定為它們的父載入器

  • 出於安全考慮,bootstrap 啟動類載入器只載入包名為 javajavaxsun 開頭的類

擴充套件類載入器(Extension ClassLoader)

  • Java 語言編寫,由 sun.misc.Launcher$ExtClassLoader 實現

  • 派生於 ClassLoader 類

  • 父類載入器為啟動類載入器

  • 從 java.ext.dirs 系統屬性所指定的目錄中載入類庫,或從 JDK 的安裝目錄的 jre/lib/ext 子目錄(擴充套件目錄)下載入類庫。如果使用者建立的 JAR 放在此目錄下,也會自動由擴充套件類載入器載入。

應用程式類載入器(系統類載入器,App ClassLoader)

  • Java 語言編寫,由 sun.misc.Launcher$AppClassLoader 實現

  • 派生於 ClassLoader 類

  • 父類載入器為擴充套件類載入器

  • 它負責載入環境變數 classpath 或系統屬性 java.class.path 指定路徑下的類庫

  • 該類載入器是程式中預設的類載入器,一般來說,Java 應用的類都是由它來完成載入

  • 通過 ClassLoader.getSystemClassLoader() 方法可以獲取到該類載入器

使用者自定義類載入器

在 Java 的日常應用程式開發中,類的載入幾乎是由上述3種類載入器相互配合執行的,在必要時,我們還可以自定義類載入器,來定製類的載入方式。

獲取 ClassLoader 的途徑

方式 程式碼
獲取當前類的 ClassLoader clazz.getClassLoader()
獲取當前執行緒上下文的 ClassLoader Thread.currentThread().getContextClassLoader()
獲取系統的 ClassLoader ClassLoader.getSystemClassLoader()
獲取呼叫者的 ClassLoader DriverManager.getCallerClassLoader()

雙親委派機制

Java 虛擬機器對 class 檔案採用的是按需載入的方式,也就是說當需要使用該類時才會將它的class檔案載入到記憶體生成 class 物件。而且載入某個類的 class 檔案時,Java 虛擬機器採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。

工作原理

1、如果一個類載入器收到類載入請求,它不會自己先載入,而是委託給父類的載入器去執行

2、如果父類載入器還存在父類載入器,則進一步委託,直到到達最頂層的類載入器

3、如果父類載入器可以完成類的載入任務,就成功返回,如果不能完成再回退到子類載入器判斷

作用

1、避免類的重複載入

比如A、B類都需要載入 String 類,如果不用委託而是自己載入自己的,則會在記憶體中生成兩份位元組碼。

2、保護程式安全,防止核心 API 被隨意篡改

比如我們在自定義一個 java.lang.String 類,執行 main 方法的時候會報錯,因為 String 是 java.lang 包下的類,應該由啟動類載入器載入。

/*
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
   public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充套件javafx.application.Application
*/
public class String {
    public static void main(String[] args) {
        System.out.println("hello string");
    }
}

如果在 java.lang 包中定義 Jdk 中不存在的類呢?依然會報錯

/*
異常:java.lang.SecurityException: Prohibited package name: java.lang
*/
public class Other {
    public static void main(String[] args) {
        System.out.println("hello other");
    }
}

相關文章