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

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");
    }
}