JVM中的類載入器子系統

夕灬顏發表於2020-10-31

總覽

在這裡插入圖片描述

  • 類載入器負責從檔案或網路中載入class檔案,在class檔案的開頭必須有特定的標識才可以被識別
  • 類載入器只負責class檔案的載入,至於它是否可以執行,取決於ExecutionEngine決定
  • Class File 載入到JVM中被稱為DNA後設資料模板,存放在方法區。方法區除了存放類的基本資訊外,還可以存放執行時常量池資訊,可能還包括字串字面量和資料常量(這部分常量資訊是class檔案中常量池部分的記憶體對映)

類載入階段概述

  1. 通過一個類的全限定名獲取定義此類的二進位制位元組流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  3. 在記憶體中生成一個代表這個類的java.lang.class物件,作為方法區這個類的各種資料的訪問入口

載入.class檔案的方式

  1. 從本地磁碟中直接載入
  2. 通過網路獲取,典型場景: web 應用
  3. 從zip壓縮包中讀取,成為日後jar、war格式的基礎
  4. 執行時計算生成,使用最多的是:動態代理技術
  5. 由其他檔案生成,典型場景:JSP應用
  6. 從專有資料庫中提取.class檔案,比較少見
  7. 從加密檔案中獲取,典型的防Class檔案被反編譯的保護措施,如安卓應用

類載入器的分類

  • JVM支援兩種型別的類載入器,分別為引導類載入器(BootstrapClassLoader)和自定義類載入器(User-Defined classLoader)。
  • 從概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類類載入器,但是Java虛擬機器規範卻沒有這麼定義,而是將所有派生於抽象類classLoader的類載入器都劃分為自定義類載入器。
  • 無論類載入器的型別如何劃分,在程式中我們最常見的類載入器始終只有3個,如下所示:

在這裡插入圖片描述

  • 引導類載入器(Bootstrap ClassLoader)

    • 這個類載入使用c/C++語言實現的,巢狀在JVM內部。
    • 它用來載入Java的核心庫,只載入包名為java、 javax、sun等開頭的類),用於提供JVM自身需要的類
    • 並不繼承自java.lang.classLoader,沒有父載入器
    • 載入擴充套件類和系統加戟器,並指定為他們的父類載入器。
  • 擴充套件類載入器(Ext ClassLoader):負責載入JAVA_HOME/lib/ext目錄下的類庫

  • 我們自己定義的類是使用:<系統類載入器>進行載入的

  • Java的核心類庫都是使用:<引導類載入器>進行載入的

    public static void main(String[] args) {
//        獲取系統類載入器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);// sun.misc.Launcher$AppClassLoader@18b4aac2

//        獲取其上層:擴充套件類載入器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);// sun.misc.Launcher$ExtClassLoader@1b6d3586

//        獲取其上層:引導類載入器 --> 獲取不到
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);// null

        /**
         * 驗證類是使用什麼載入器
         */
//        我們自己定義的類是使用:<系統類載入器>進行載入的
        ClassLoader myClass = ClassLoaderType.class.getClassLoader();
        System.out.println(myClass);// sun.misc.Launcher$AppClassLoader@18b4aac2

//        Java的核心類庫都是使用:<引導類載入器>進行載入的
        ClassLoader sysClass = String.class.getClassLoader();
        System.out.println(sysClass);// null

    }

雙親委派機制

Java虛擬機器在載入class檔案時是採用按需載入的方式進行載入的,而載入這個class檔案時,就是採用的雙親委派機制,其流程如下:

  1. 當一個類載入器收到載入類的請求時,它並不是直接去載入這個類,而是把這個請求委託給父類載入器去執行,如果存在父類載入器還存在父類載入器,則進一步向上委託,直到最頂層的引導類載入器。
  2. 如果父類載入器可以完成該類的載入,就只用該載入器進行載入,若父類載入器不能進行載入這個類,就把載入這個類交給子載入器去載入,這就是雙親委派機制。

如我們自己新建一個java,lang包,在該包下新建一個String類,當我們只用這個String類的時候,系統類載入器是不會去載入我們編寫的這個類的,因為這個java.lang.String這個類被引導類載入器進行了載入。

優點:

  1. 可以避免類被重複載入
  2. 可以保護程式的安全,防止核心API被隨機篡改。如我們新建一個java.lang包,並在這個包下定義自己的類,引導類載入器是不會載入我們自定義的類的,會丟擲一個安全異常。其實這就是沙箱安全機制的體現

在這裡插入圖片描述

使用者自定義類載入器

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

  • 隔離載入類
  • 修改類載入的方式
  • 擴充套件載入源
  • 防止原始碼洩漏

使用者自定義類載入器實現步驟:

  1. 開發人員可以通過繼承抽象類java.lang.classLoader類的方式,實現自己的類載入器,以滿足一些特殊的需求
    2 在JDK1.2之前,在自定義類載入器時,總會去繼承classLoader類並重寫loadclass ()方法,從而實現自定義的類載入類,但是在JDK1.2之後,不再建議使用者去覆蓋loadclass ()方法,而是建議把自定義的類載入邏輯寫在findclass ()方法中
  2. 在編寫自定義類載入器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findclass()方法及其獲取位元組碼流的方式,使自定義類載入器編寫更加簡潔。

classLoader類,它是一個抽象類,其後所有的類載入器都繼承自classLoader (不包括啟動類載入器

在這裡插入圖片描述

連結階段的三個過程

驗證

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

準備

  • 例項變數在準備階段會賦初始預設值,在初始化階段才真正賦值
  • 類變數(static修飾)分配在方法區中,而例項物件是隨著物件的建立一起分配到java堆中
  • static final修飾的變數在編譯的時候已經初始化了,在準備階段就真正賦值了

解析

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

初始化階段概述

  • 初始化階段就是執行類構造器方法())的過程

    • 此方法不需定義,是javac編譯器自動收集類中所有例項變數的賦值動作和靜態程式碼塊中的語句合併而來,並且會保證構造器方法中指令會按照原始檔中出現的順序執行。
  • ()不同於類的構造器。(構造方法是虛擬機器視角下的())

  • 若該類具有父類,JVM會保證父類的()先執行,再子類的()

  • 虛擬機器會保證一個類的()方法在多執行緒下被同步加鎖。

/*
    結果為100
*/
public static int a = 10;

static {
    a = 100;
}

public static void main(String[] args) {
    System.out.println(a);
}

// *********************************
/*
    結果為10
*/

static {
    a = 100;
}

public static int a = 10;

public static void main(String[] args) {
    System.out.println(a);
}

相關文章