【JVM以內存與垃圾回收篇】類加載子系統

類加載子系統

概述

完整圖以下:java

若是本身想手寫一個 Java 虛擬機的話,主要考慮哪些結構呢?數據庫

  • 類加載器
  • 執行引擎

類加載器子系統做用

類加載器子系統負責從文件系統或者網絡中加載 Class 文件,Class 文件在文件開頭有特定的文件標識(CAFE BABE)。bootstrap

ClassLoader 只負責 Class 文件的加載,至於它是否能夠運行,則由 Execution Engine 決定。數組

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

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

類的加載過程

例以下面的一段簡單的代碼bash

/**
 * 類加載子系統
 * @author: Nemo
 */
public class HelloLoader {
    public static void main(String[] args) {
        System.out.println("我已經被加載啦");
    }
}

它的加載過程是怎麼樣的呢?網絡

完整的流程圖以下所示數據結構

加載階段

  1. 經過一個類的全限定名獲取定義此類的二進制字節流多線程

  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構app

  3. 在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這個類的各類數據的訪問入口

加載.class文件的方式

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

連接階段

驗證(Verify)

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

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


工具:Binary Viewer 查看

若是出現不合法的字節碼文件,那麼將會驗證不經過

同時咱們能夠經過安裝 IDEA 的插件,來查看咱們的 Class 文件

安裝完成後,咱們編譯完一個 class 文件後,點擊 view 便可顯示咱們安裝的插件來查看字節碼方法了

準備(Prepare)

  • 爲類變量分配內存而且設置該類變量的默認初始值,即零值。

  • 這裏不包含用 final 修飾的 static,由於 final 在編譯的時候就會分配了,準備階段會顯式初始化;

  • 這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到 Java 堆中。


例以下面這段代碼

/**
 * @author: Nemo
 */
public class HelloApp {
    private static int a = 1;  // 準備階段爲0,在下個階段,也就是初始化的時候纔是1
    public static void main(String[] args) {
        System.out.println(a);
    }
}

上面的變量 a 在準備階段會賦初始值,但不是 1,而是 0。

解析(Resolve)

  • 將常量池內的符號引用轉換爲直接引用的過程。

  • 事實上,解析操做每每會伴隨着 JVM 在執行完初始化以後再執行。

  • 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明肯定義在《Java 虛擬機規範》的 Class 文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

  • 解析動做主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的 CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

初始化階段

初始化階段就是執行類構造器法 <clinit>() 的過程。

此方法不需定義,是 javac 編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併而來。

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

構造器方法中指令按語句在源文件中出現的順序執行。

<clinit>() 不一樣於類的構造器。(關聯:構造器是虛擬機視角下的 <init>())若該類具備父類,JVM 會保證子類的 <clinit>() 執行前,父類的 <clinit>() 已經執行完畢。

任何一個類在聲明後,都有生成一個構造器,默認是空參構造器

/**
 * @author: Nemo
 */
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;

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

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

/**
 * @author: Nemo
 */
public class ClinitTest1 {
    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) {
        System.out.println(Son.b);
    }
}

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

iconst_1
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
iconst_2
putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A>
return

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

/**
 * @author: Nemo
 */
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 初始化當前類

從上面能夠看出初始化後,只可以執行一次初始化,這也就是同步加鎖的過程

類加載器的分類

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

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

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

這裏的四者之間是包含關係,不是上層和下層,也不是子系統的繼承關係。

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

/**
 * @author: Nemo
 */
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@1540e19d
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

虛擬機自帶的加載器

啓動類加載器(引導類加載器,Bootstrap ClassLoader)

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

擴展類加載器(Extension ClassLoader)

  • Java 語言編寫,由 sun.misc.Launcher$ExtClassLoader 實現。
  • 派生於 ClassLoader 類
  • 父類加載器爲啓動類加載器
  • 從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,或從 JDK 的安裝目錄的 jre/1ib/ext 子目錄(擴展目錄)下加載類庫。若是用戶建立的 JAR 放在此目錄下,也會自動由擴展類加載器加載。

應用程序類加載器(系統類加載器,AppClassLoader)

  • java 語言編寫,由 sun.misc.LaunchersAppClassLoader 實現
  • 派生於 ClassLoader 類
  • 父類加載器爲擴展類加載器
  • 它負責加載環境變量 classpath 或系統屬性 java.class.path 指定路徑下的類庫
  • 該類加載是程序中默認的類加載器,通常來講,Java 應用的類都是由它來完成加載
  • 經過 classLoader#getSystemclassLoader() 方法能夠獲取到該類加載器

用戶自定義類加載器

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

爲何要自定義類加載器?

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

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

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

查看根加載器所能加載的目錄

剛剛咱們經過概念瞭解到了,根加載器只可以加載 java /lib 目錄下的 class,咱們經過下面代碼驗證一下

/**
 * @author: Nemo
 */
public class ClassLoaderTest1 {
    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();
    }
}

獲得的結果

*********啓動類加載器************
file:/E:/Software/JDK1.8/Java/jre/lib/resources.jar
file:/E:/Software/JDK1.8/Java/jre/lib/rt.jar
file:/E:/Software/JDK1.8/Java/jre/lib/sunrsasign.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jsse.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jce.jar
file:/E:/Software/JDK1.8/Java/jre/lib/charsets.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jfr.jar
file:/E:/Software/JDK1.8/Java/jre/classes
null

關於ClassLoader

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

方法名稱 概述
getParent() 返回該類加載器的超類加載器
loadClass(Sting 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 類

sun.misc.Launcher 它是一個 java 虛擬機的入口應用

獲取 ClassLoader 的途徑

  • 方法一:獲取當前 ClassLoader
    clazz.getClassLoader()

    通常用 clazz 表示一個類的實例,而 class 只是個關鍵字

  • 方法二:獲取當前線程上下文的 ClassLoader
    Thread.currentThread().getContextClassLoader()
  • 方法三:獲取系統的 ClassLoader
    ClassLoader.getSystemClassLoader()
  • 方法四:獲取調用者的 ClassLoader
    DriverManager.getCallerClassLoader()

雙親委派機制

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

工做原理

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

雙親委派機制舉例

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

雙親委派機制的優點

經過上面的例子,咱們能夠知道,雙親機制能夠

  • 避免類的重複加載
  • 保護程序安全,防止核心 API 被隨意篡改
    • 自定義類:java.lang.String
    • 自定義類:java.lang.ShkStart(報錯:阻止建立 java.lang 開頭的類)

沙箱安全機制

沙盒(英語:sandbox,又譯爲沙箱),計算機術語,在計算機安全領域中是一種安全機制,爲運行中的程序提供的隔離環境。

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

其它

如何判斷兩個 class 對象是否相同

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

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

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

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

類的主動使用和被動使用

Java 程序對類的使用方式分爲:王動使用和被動使用。

主動使用,又分爲七種狀況:

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

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

相關文章
相關標籤/搜索