天天看點

JVM-類加載子系統類加載子系統

文章目錄

  • 類加載子系統
    • 1.JVM記憶體結構圖
    • 2.什麼是類加載子系統
        • 2.1 加載階段
            • 2.1.1 類加載器ClassLoader
            • 2.2.2 類加載階段過程
        • 2.2 連結階段
            • 2.2.1 驗證(Verify)
            • 2.2.2 準備(Prepare)
            • 2.2.3 解析(Resolve)
        • 2.3 初始化階段
    • 3.類加載器的分類
        • 3.1 虛拟機自帶的加載器
            • 3.1.1 啟動類加載器
            • 3.1.2 擴充類加載器
            • 3.1.3 系統類加載器
        • 3.2 使用者自定義類加載器
            • 3.2.1 什麼時候需要自定義類加載器?
            • 3.2.2 如何自定義類加載器?
    • 4.雙親委派機制
        • 4.1 什麼是雙親委派機制
        • 4.2 雙親委派機制原理
        • 4.2.3 雙親委派機制優勢
    • 5.沙箱安全機制
    • 6.補充

類加載子系統

1.JVM記憶體結構圖

JVM-類加載子系統類加載子系統

2.什麼是類加載子系統

類加載器子系統負責從檔案系統或者網絡中加載Class檔案,class檔案在檔案開頭有特定的檔案辨別。當中的類加載器隻負責class檔案的加載,至于它是否可以運作,則由Execution Engine(執行引擎)決定。加載的類資訊存放于一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放運作時常量池資訊,可能還包括字元串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體映射)

JVM-類加載子系統類加載子系統

舉例來說類加載子系統就像是一個中央快遞站,當快遞被打包好(編譯後)發送過來的時候,去進行接收,首先收到快遞看是什麼類型的快遞(順豐,郵政等),不同的快遞由不同的人員去接收(不同的類由不同的加載器去加載),接收完成後要進行驗證,看是不是有什麼損壞(連結階段----驗證),當一切無誤後為該快遞貼上取貨碼(連結階段----準備:初始化一些資訊比如類變量),再然後檢視快遞所要去往的地方,由快遞員去派送(連結階段----解析:将常量池内的符号引用轉換為直接引用的過程),到達目的快遞站後交由本地快遞站進行處理(進入到初始化階段),快遞可以由快遞站直接送往顧客家裡(類的被動使用),也可以由顧客主動來領(類的主動使用例如:建立類的執行個體,調用類的靜态方法等).

當然在類的加載階段還有雙親委派機制,在後面會提到.

2.1 加載階段

2.1.1 類加載器ClassLoader

  • class file(編譯後的檔案)存在于本地硬碟上,可以了解為設計師畫在紙上的模闆,而最終這個模闆在執行的時候是要加載到JVM當中來根據這個檔案執行個體化出n個一模一樣的執行個體。
  • class file加載到JVM中,被稱為DNA中繼資料模闆放在方法區。
  • 在.class檔案–>JVM–>最終成為中繼資料模闆,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。

2.2.2 類加載階段過程

public class Loader {

    public static void main(String[] args) {
        System.out.println("謝謝ClassLoader加載我....");
    }
}
           

對于上面的代碼他的加載過程是什麼樣呢?

  • 首先要想執行 main() 方法(靜态方法)就需要先加載main方法所在類 Loader
  • 如果加載成功,則進行連結、初始化等操作。完成後調用 Loader類中的靜态方法 main
  • 加載失敗則會抛出異常
    JVM-類加載子系統類加載子系統

2.2 連結階段

連結分為三個子階段:驗證 -> 準備 -> 解析

2.2.1 驗證(Verify)

  • 目的在于確定Class檔案的位元組流中包含資訊符合目前虛拟機要求,保證被加載類的正确性,不會危害虛拟機自身安全
  • 主要包括四種驗證,檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。

比如說如果你檢視java編譯後的位元組碼檔案就會發現,它們的開頭都是CAFE BABE(很多人稱之為咖啡寶貝),如果出現不合法的位元組碼檔案,那麼将會驗證不通過。

2.2.2 準備(Prepare)

  • 為類變量(static變量)配置設定記憶體并且設定該類變量的預設初始值
  • 當然這裡不包含用final修飾的static,因為final在編譯的時候就會配置設定好了預設值,準備階段會顯式初始化
  • 同時要注意這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量是會随着對象一起配置設定到Java堆中

舉例來說 檢視編譯後的檔案

編譯前

package com.Demo;

public class ClassInitTest {
    public static int num = 3;

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

編譯後

public com.Demo.ClassInitTest();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0	//可以看到在初始化階段預設賦了初值為0
         1: invokespecial #1                 
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/Demo/ClassInitTest;
           
static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_3	//在初始化階段才指派為了3
         1: putstatic     #3                  // Field num:I
         4: return
      LineNumberTable:
        line 4: 0
}

           

2.2.3 解析(Resolve)

  • 将常量池内的符号引用轉換為直接引用的過程
  • 事實上,解析操作往往會伴随着JVM在執行完初始化之後再執行
  • 符号引用就是一組符号來描述所引用的目标。符号引用的字面量形式明确定義在《java虛拟機規範》的class檔案格式中。直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄
  • 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

通過反編譯class檔案可以檢視到符号引用

#1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
  #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
  #3 = Fieldref           #5.#26         // com/Demo/ClassInitTest.num:I
  #4 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
  #5 = Class              #29            // com/Demo/ClassInitTest
  #6 = Class              #30            // java/lang/Object
  #7 = Utf8               num
  #8 = Utf8               I
  #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/Demo/ClassInitTest;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
           

2.3 初始化階段

類的初始化時機有:

  • 建立類的執行個體
  • 通路某個類或接口的靜态變量,或者對該靜态變量指派
  • 調用類的靜态方法
  • 反射(比如:Class.forName(“TestClass”))
  • 初始化一個類的子類
  • Java虛拟機啟動時被标明為啟動類的類
  • JDK7開始提供的動态語言支援:java.lang.invoke.MethodHandle執行個體的解析結果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化

除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導緻類的初始化,即不會執行初始化階段(不會調用 clinit() 方法和 init() 方法)

clinit()方法

  • 初始化階段就是執行類構造器方法

    <clinit>()

    的過程
  • 此方法不需定義,是javac編譯器自動收集類中的所有類變量的指派動作和靜态代碼塊中的語句合并而來。也就是說,當我們代碼中包含static變量的時候,就會有clinit方法
  • <clinit>()

    方法中的指令按語句在源檔案中出現的順序執行
  • <clinit>()

    不同于類的構造器。(關聯:構造器是虛拟機視角下的

    <init>()

  • 若該類具有父類,JVM會保證子類的

    <clinit>()

    執行前,父類的

    <clinit>()

    已經執行完畢
  • 虛拟機必須保證一個類的

    <clinit>()

    方法在多線程下被同步加鎖

3.類加載器的分類

JVM嚴格來講支援兩種類型的類加載器 。分别為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)

從概念上來講,自定義類加載器一般指的是程式中由開發人員自定義的一類類加載器,但是Java虛拟機規範卻沒有這麼定義,而是将所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器

JVM-類加載子系統類加載子系統

可以看到所有ClassLoader下的所有派生類都是屬于自定義類加載器,包括擴充類加載器(Extension ClassLoader)以及系統類加載器(Application ClassLoader)

在程式中我們最常見的類加載器隻有3個分别是ExtClassLoader,AppClassLoader,使用者自定義加載器

3.1 虛拟機自帶的加載器

3.1.1 啟動類加載器

啟動類加載器(引導類加載器,Bootstrap ClassLoader)
  • 這個類加載使用C/C++語言實作的,嵌套在JVM内部
  • 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的内容),用于提供JVM自身需要的類
  • 并不繼承自java.lang.ClassLoader,沒有父加載器
  • 加載擴充類和應用程式類加載器,并作為他們的父類加載器
  • 出于安全考慮,Bootstrap啟動類加載器隻加載包名為java、javax、sun等開頭的類

3.1.2 擴充類加載器

擴充類加載器(Extension ClassLoader)
  • Java語言編寫,由sun.misc.Launcher$ExtClassLoader實作
  • 派生于ClassLoader類
  • 父類加載器為啟動類加載器
  • 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴充目錄)下加載類庫。如果使用者建立的JAR放在此目錄下,也會自動由擴充類加載器加載

3.1.3 系統類加載器

應用程式類加載器(也稱為系統類加載器,AppClassLoader)
  • Java語言編寫,由sun.misc.LaunchersAppClassLoader實作
  • 派生于ClassLoader類
  • 父類加載器為擴充類加載器
  • 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
  • 該類加載是程式中預設的類加載器,一般來說,Java應用的類都是由它來完成加載
  • 通過classLoader.getSystemclassLoader()方法可以擷取到該類加載器

3.2 使用者自定義類加載器

3.2.1 什麼時候需要自定義類加載器?

  • 隔離加載類(比如說我假設現在Spring架構,和RocketMQ有包名路徑完全一樣的類,類名也一樣,這個時候類就沖突了。不過一般的主流架構和中間件都會自定義類加載器,實作不同的架構,中間價之間是隔離的)
  • 修改類加載的方式
  • 擴充加載源(還可以考慮從資料庫中加載類,路由器等等不同的地方)
  • 防止源碼洩漏(對位元組碼檔案進行解密,自己用的時候通過自定義類加載器來對其進行解密)

3.2.2 如何自定義類加載器?

  • 開發人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實作自己的類加載器,以滿足一些特殊的需求
  • 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass()方法,進而實作自定義的類加載類,但是在JDK1.2之後已不再建議使用者去覆寫loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
  • 在編寫自定義類加載器時,如果沒有太過于複雜的需求,可以直接繼承URIClassLoader類,這樣就可以避免自己去編寫findclass()方法及其擷取位元組碼流的方式,使自定義類加載器編寫更加簡潔。
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                //defineClass和findClass搭配使用
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }
	//自定義流的擷取方式
    private byte[] getClassFromCustomPath(String name) {
        //從自定義路徑中加載指定類:細節略
        //如果指定路徑的位元組碼檔案進行了加密,則需要在此方法中進行解密操作。
        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One", true, customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

4.雙親委派機制

4.1 什麼是雙親委派機制

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

4.2 雙親委派機制原理

  1. 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行;
  2. 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終将到達頂層的啟動類加載器;
  3. 如果父類加載器可以完成類加載任務,就成功傳回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
  4. 父類加載器一層一層往下配置設定任務,如果子類加載器能加載,則加載此類,如果将加載任務配置設定至系統類加載器也無法加載此類,則抛出異常
    JVM-類加載子系統類加載子系統

例如:

我們自己建立一個 java.lang.String 類,寫上 static 代碼塊

package java.lang;
public class String {
    static{
        System.out.println("自定義的String類的靜态代碼塊");
    }
}
           

在另外的程式中加載 String 類

public class StringTest {
    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello String");
    }
}
           

輸出結果:

并沒有列印自定義的String 類中的語句,是以系統加載的還是JDK 自帶的 String 類.

把剛剛的類改一下

package java.lang;
public class String {
    static{
        System.out.println("自定義的String類的靜态代碼塊");
    }
    public static void main(String[] args) {
        System.out.println("hello String");
    }
}
           
JVM-類加載子系統類加載子系統

由于雙親委派機制會一直找父類加載器,是以最後找到了Bootstrap ClassLoader(引導類加載器),Bootstrap ClassLoader找到的是 JDK 自帶的 String 類,在那個String類中并沒有相應的 main() 方法,是以就報了上面的錯誤。

4.2.3 雙親委派機制優勢

  • 避免類的重複加載
  • 保護程式安全,防止核心API被随意篡改

5.沙箱安全機制

  1. 自定義String類時:在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的檔案(rt.jar包中java.lang.String.class),報錯資訊說沒有main方法,就是因為加載的是rt.jar包中的String類。
  2. 這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。

6.補充

如果要判斷兩個class對象是否相同,在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:

  1. 類的完整類名必須一緻,包括包名
  2. 加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同
  3. 換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class檔案,被同一個虛拟機所加載,但隻要加載它們的ClassLoader執行個體對象不同,那麼這兩個類對象也是不相等的

對類加載器的引用

  1. JVM必須知道一個類型是由啟動加載器加載的還是由使用者類加載器加載的
  2. 如果一個類型是由使用者類加載器加載的,那麼JVM會将這個類加載器的一個引用作為類型資訊的一部分儲存在方法區中
  3. 當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的