【JVM學習】——類加載器子系統

1、類加載器子系統

1.1 JVM體系結構

JVM被分爲三個主要的子系統:html

(1)類加載器子系統(2)運行時數據區(3)執行引擎java

1.2 類加載器子系統做用

(1)類加載子系統負責從文件系統或者網絡中加載class文件,class文件在文件開有特定的文件標識(0xCAFEBABE)。數據庫

(2)類加載器(Class Loader)只負責class文件的加載,至於它是否能夠運行,則由執行引擎(Execution Engine)決定。bootstrap

(3)加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)。數組

(4)Class對象是存放在堆區的。安全

假若有一個Car.java文件,編譯後生成一個Car.class字節碼文件:bash

  • class file存在於本地硬盤上,能夠理解爲一個模板。而這個模板在執行的時候是須要加載到JVM當中,JVM再根據這個模板實例化出N個如出一轍的實例。
  • class file加載到JVM中後,被稱爲DNA元數據模板,放在方法區。
  • 在.class文件-->JVM-->最終成爲元數據模板,此過程就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色。

1.3 類的加載過程

一個類型從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱爲鏈接(Linking)。完整的流程圖以下所示:網絡

加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的。爲了支持Java語言的運行時綁定解析階段也能夠是在初始化以後進行的。(以上順序流程指的是程序開始的順序,在實際運行中,這些階段一般都是互相交叉地混合進行的,會在一個階段執行的過程當中調用、激活另外一個階段)。數據結構

(1)加載階段

「加載」(Loading)階段是整個「類加載」(Class Loading)過程當中的一個階段,JVM須要完成三件事:多線程

  • 經過一個類的全限定名獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

加載class文件的方式

  • 從本地系統中直接加載。
  • 經過網絡獲取,典型場景:Web Applet。
  • 從zip壓縮包中讀取,成爲往後jar、war格式的基礎。
  • 運行時計算生成,使用最多的是:動態代理技術。
  • 由其餘文件生成,典型場景:JSP應用從專有數據庫中提取.class文件,比較少見。
  • 從加密文件中獲取,典型的防Class文件被反編譯的保護措施。
  • ... ...

(2)連接階段

  1. 驗證 Verify

目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。

主要包括四種驗證:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

文件格式驗證

第一階段要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。

  • 是否以魔數0xCAFEBABE開頭。
  • 主、次版本號是否在當前Java虛擬機接受範圍以內。
  • 常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)。
  • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數據。
  • Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息。

元數據驗證

第二階段是對字節碼描述的信息進行語義分析。

  • 這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類)。
  • 這個類的父類是否繼承了不容許被繼承的類(被final修飾的類)。
  • 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法。
  • 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)。

字節碼驗證

經過數據流分析和控制流分析,肯定程序語義是合法的、符合邏輯的。

  • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似於「在操做棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中」這樣的狀況。
  • 保證任何跳轉指令都不會跳轉到方法體之外的字節碼指令上。
  • 保證方法體中的類型轉換老是有效的。

符號引用驗證

對類自身之外(常量池中的各類符號引用)的各種信息進行匹配性校驗,通俗來講就是,該類是否缺乏或者被禁止訪問它依賴的某些外部類、方法、字段等資源。

  • 符號引用中經過字符串描述的全限定名是否能找到對應的類。
  • 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
  • 符號引用中的類、字段、方法的可訪問性(private、protected、public、<package>)是否可被當前類訪問。

咱們能夠經過安裝IDEA的插件——jclasslib Bytecode viewer,來查看咱們的Class文件:

安裝完成後,咱們編譯完一個class文件後,點擊View--> Show Bytecode With Jclasslib便可顯示咱們安裝的插件來查看字節碼。

  1. 準備 Prepare
  • 爲類變量分配內存而且設置該類變量的默認初始值,即零值。(Boolean類型數據的零值爲False)

例以下面這段代碼:

public class Hello {
    private static int a = 1;  // 準備階段爲0,在下個階段,也就是初始化的時候纔是1。
    public static void main(String[] args) {
        System.out.println(a);
    }
}
  • 這裏不包含用final修飾的static,由於final在編譯的時候就會分配了,準備階段會顯式初始化
  • 這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中。
  1. 解析 Resolve
  • 將常量池內的符號引用轉換爲直接引用的過程
  • 事實上,解析操做每每會伴隨着JVM在執行完初始化以後再執行。
  • 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明肯定義在《java虛擬機規範》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
  • 解析動做主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT_Methodref_info等。

(3)初始化階段

初始化階段就是執行類構造器法<clinit>()的過程。此方法不需定義,是javac編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊(static{}塊)中的語句合併而來,編譯器收集的順序是由語句在源文件中出現的順序決定的。

  • 也就是說,當咱們代碼中包含static變量的時候,就會有<clinit>()方法

<clinit>()不一樣於類的構造器函數。(關聯:構造器函數是虛擬機視角下的<init>()方法。若該類具備父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢。所以在Java虛擬機中第一個被執行的<clinit>()方法的類型確定是java.lang.Object

  • 任何一個類在聲明後,都有生成一個構造器,默認是空參構造器
public class ClassInitTest {
    private static int num = 1;
    static {
        num = 2;
        number = 20;
        System.out.println(num);
        System.out.println(number);  //報錯,非法的前向引用
    }

    private static int number = 10; // prepare:number = 0--> number-->initial: 20-->10

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

關於涉及到父類時候的變量賦值過程:

public class ClinitTest {
    static class Father {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Son extends Father {
        public static int B = A;
    }

    public static void main(String[] args) {
        // 加載Father類,其次加載Son類
        System.out.println(Son.B);
    }
}

咱們輸出結果爲 2,也就是說首先加載ClinitTest的時候,會找到main方法,而後執行Son的初始化,可是Son繼承了Father,所以還須要執行Father的初始化,同時將A賦值爲2。咱們經過反編譯獲得Father的加載過程,首先咱們看到原來的值被賦值成1,而後又被複製成2,最後返回:

iconst_1
putstatic #2 <com/kai/jvm/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/kai/jvm/ClinitTest1$Father.A>
return

虛擬機必須保證一個類的<clinit>()方法在多線程下被同步加鎖。

public class DeadThreadTest {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 線程t1開始");
            new DeadThread();
        }, "t1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 線程t2開始");
            new DeadThread();
        }, "t2").start();
    }
}
class DeadThread {
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "\t 初始化當前類");
            while(true) {
            }
        }
    }
}

上面的代碼,輸出結果爲:

線程t1開始
線程t2開始
線程t2 初始化當前類

從上面能夠看出只可以執行一次初始化,其中一條線程一直在阻塞等待。

2、類加載器

2.1 類加載器的分類

在類加載階段中,實現「經過一個類的全限定名來獲取描述該類的二進制字節流」這個動做的代碼就被稱爲「類加載器」(ClassLoader)。

JVM支持兩種類型的類加載器 ,分別爲啓動類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。

從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類類加載器,可是Java虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類ClassLoader的類加載器都劃分爲自定義類加載器。

不管類加載器的類型如何劃分,在程序中咱們最多見的類加載器主要有3類,以下所示:

Tips:各種加載器之間的關係不是傳統意義上的繼承關係。

咱們經過一個類,獲取不一樣的加載器:

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 獲取系統類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        // 獲取擴展類加載器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);

        // 獲取啓動類加載器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);

        // 獲取自定義加載器
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);
        
        // 獲取String類型的加載器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);
    }
}

獲得的結果,從結果能夠看出啓動類加載器沒法經過代碼直接獲取,同時目前用戶代碼所使用的加載器爲系統類加載器。同時咱們經過獲取String類型的加載器,發現是null,這間接說明了String類型是經過啓動類加載器進行加載的。(Java的核心類庫都是使用啓動類加載器進行加載的

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4554617c
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

2.2 虛擬機自帶的加載器

  1. 啓動類加載器(引導類加載器,Bootstrap ClassLoader)
  • 這個類加載使用C/C++語言實現的,嵌套在JVM內部。
  • 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jarresources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身須要的類。
  • 並不繼承自ava.lang.ClassLoader,沒有父加載器。
  • 加載擴展類和應用程序類加載器,並指定爲他們的父類加載器。
  • 出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類。

咱們經過下面代碼驗證一下:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println("*********啓動類加載器************");
        // 獲取BootstrapClassLoader可以加載的API的路徑
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url.toExternalForm());
        }

        // 從上面路徑中,隨意選擇一個類,來看看他的類加載器是什麼:獲得的是null,則說明是啓動類加載器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);
    }
}

獲得的結果(%20是空格):

*********啓動類加載器************
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/classes
null
  1. 擴展類加載器(Extension ClassLoader)
  • Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現。
  • 派生於ClassLoader類。
  • 父類加載器爲啓動類加載器。
  • 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。若是用戶建立的JAR放在此目錄下,也會自動由擴展類加載器加載。

咱們經過下面代碼驗證一下:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println("*********擴展類加載器************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }

        // Java\lib\ext目錄下隨意選擇一個類,查看他的類加載器是什麼
        ClassLoader classLoader = CurveDB.class.getClassLoader();
        System.out.println(classLoader);
    }
}

獲得的結果:

*********擴展類加載器************
C:\Program Files\Java\jdk1.8.0_151\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@7ea987ac
  1. 系統類加載器(應用程序類加載器,AppClassLoader)
  • Java語言編寫,由sun.misc.Launcher¥AppClassLoader實現。
  • 派生於ClassLoader類。
  • 父類加載器爲擴展類加載器。
  • 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫。
  • 該類加載是程序中默認的類加載器,通常來講,Java應用的類都是由它來完成加載。
  • 經過classLoader#getSystemclassLoader()方法能夠獲取到該類加載器。

2.3 用戶自定義類加載器

在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,來定製類的加載方式。
爲何要自定義類加載器?

  • 隔離加載類
  • 修改類加載的方式
  • 擴展加載源
  • 防止源碼泄漏

用戶自定義類加載器實現步驟:

  • 開發人員能夠經過繼承抽象類ava.lang.ClassLoader類的方式,實現本身的類加載器,以知足一些特殊的需求
  • 在JDK1.2以前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋1oadclass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
  • 在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URIClassLoader類,這樣就能夠避免本身去編寫findclass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

2.4 ClassLoader類

ClassLoader類,它是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器)。

方法名稱 描述
getParent() 返回該類加載器的超類加載器
loadClass(String name) 加載名稱爲name的類,返回結果爲java.lang.Class類的實例
findClass(String name) 查找名稱爲name的類,返回結果爲java.lang.Class類的實例
findLoadedClass(String name) 查找名稱爲name的已經被加載過的類,返回結果爲java.lang.Class類的實例
defineClass(String name, byte[] b, int off, int len) 把字節數組b中的內容轉換爲一個Java類,返回結果爲java.lang.Class類的實例
resolveClass(Class<?> c) 鏈接指定的一個Java類

獲取ClassLoader的途徑:

  • 獲取當前ClassLoader:clazz.getClassLoader()
  • 獲取當前線程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  • 獲取系統的ClassLoader:ClassLoader.getSystemClassLoader()
  • 獲取調用者的ClassLoader:DriverManager.getCallerClassLoader()

3、雙親委派機制

Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象。並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把優先將請求交由父類處理,它是一種任務委派模式。

3.1 工做原理

  • 若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行;
  • 若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器;
  • 若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式。(注意,此處的父類、子類不是繼承中父類、子類的概念。)

下面用一個例子說明:

public class StringTest {
    public static void main(String[] args) {
        String string = new String();
        System.out.println("Hello World!");
    }
}

而後自定義一個java.lang.String類:

public class String {
    static {
        System.out.println("這是自定義的String類的靜態代碼塊!");
    }
}

執行結果:Hello World!

3.2 雙親委派機制舉例

當咱們加載jdbc.jar 用於實現數據庫鏈接的時候,首先咱們須要知道的是 jdbc.jar是基於SPI接口進行實現的,因此在加載的時候,會進行雙親委派,最終從啓動類加載器中加載 SPI核心類。而後再加載SPI接口實現類,就進行反向委派,經過線程上下文類加載器進行實現jdbc.jar的加載。

3.3 雙親委派機制的優點

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

    • 自定義類:java.lang.String
    • 自定義類:java.lang.XXXX(報錯:阻止建立 java.lang開頭的類)

3.4 沙箱安全機制

Java安全模型的核心就是Java沙箱(sandbox)。沙箱是一個限制程序運行的環境。沙箱機制就是將 Java 代碼限定在虛擬機(JVM)特定的運行範圍中,而且嚴格限制代碼對本地系統資源訪問,經過這樣的措施來保證對代碼的有效隔離,防止對本地系統形成破壞。

組成Java沙箱的基本組件以下:

  • 類加載體系結構
  • class文件檢驗器
  • 內置於Java虛擬機(及語言)的安全特性
  • 安全管理器及Java API

Java安全模型的前三個部分——類加載體系結構、class文件檢驗器、Java虛擬機(及語言)的安全特性一塊兒達到一個共同的目的:保持Java虛擬 機的實例和它正在運行的應用程序的內部完整性,使得它們不被下載的惡意代碼或有漏洞的代碼侵犯。相反,這個安全模型的第四個組成部分是安全管理器,它主要 用於保護虛擬機的外部資源不被虛擬機內運行的惡意或有漏洞的代碼侵犯。這個安全管理器是一個單獨的對象,在運行的Java虛擬機中,它在對於外部資源的訪 問控制起中樞做用。

例如,自定義一個java.lang.String類,可是在加載自定義String類的時候會率先使用啓動類加載器加載,而啓動類加載器在加載的過程當中會先加載jdk自帶的文件(rt.jar包java.lang.中javalangString.class),報錯信息說沒有main方法,就是由於加載的是rt.jar包中的string類。這樣能夠保證對java核心源代碼的保護,這就是沙箱安全機制。

public class String {
    static {
        System.out.println("這是自定義的String類的靜態代碼塊!");
    }
    
    // 錯誤
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

4、補充

5.1 比較class對象

在JVM中表示兩個class對象是否爲同一個類存在兩個必要條件:

  • 類的完整類名必須一致,包括包名。
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。

換句話說,在JVM中,即便這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不一樣,那麼這兩個類對象也是不相等的。

JVM必須知道一個類型是由啓動加載器加載的仍是由用戶類加載器加載的。若是一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中。當解析一個類型到另外一個類型的引用的時候,JVM須要保證這兩個類型的類加載器是相同的。

5.2 類的主動使用和被動使用

Java程序對類的使用方式分爲:主動使用和被動使用
主動使用,又分爲七種狀況:

  • 建立類的實例
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  • 調用類的靜態方法I
  • 反射(好比:Class.forName("com.kai.Test"))
  • 初始化一個類的子類
  • Java虛擬機啓動時被標明爲啓動類的類
  • JDK7開始提供的動態語言支持:
  • java.lang.invoke.MethodHandle實例的解析結果REF getStatic、REF putStatic、REF invokeStatic句柄對應的類沒有初始化,則初始化

除了以上七種狀況,其餘使用Java類的方式都被看做是對類的被動使用,都不會致使類的初始化

參考

深刻理解Java虛擬機:JVM高級特性與最佳實踐(第3版)

java中的安全模型(沙箱機制)

相關文章
相關標籤/搜索