天天看點

JVM記憶體和垃圾回收-02.類加載子系統

1.記憶體結構概述

JVM記憶體和垃圾回收-02.類加載子系統

2.類加載子系統作用

  • 負責從檔案系統或者網絡中加載Class檔案(該檔案在檔案開頭有特定的辨別)
  • 隻負責Class檔案的加載,至于能否運作由

    Execution Engine

    決定
  • 加載的類資訊被存放在稱為方法區的記憶體空間中
tips:
  • 除了類的資訊,方法區還會存放運作時常量池資訊(可能包括字元串常量和數字常量,這部分資訊是Class檔案中常量池部分的記憶體映射)
  • 将Class檔案進行反編譯會發現結構中有常量池,将該資訊加載到記憶體中就是運作時常量池

3.類的加載過程

JVM記憶體和垃圾回收-02.類加載子系統

3.1 加載

  • 通過一個類的全限定名擷取定義此類的二進制位元組流
  • 将該位元組流代表的靜态存儲結構轉換為方法區的運作時資料結構
  • 在記憶體中生成一個代表該類的

    java.lang.Class

    對象作為方法區中該類的資料通路入口

3.2 連結

  • 驗證:確定Class檔案的位元組流中包含的資訊符合目前JVM要求,確定加載類的準确性
    • 檔案格式驗證
    • 中繼資料驗證
    • 位元組碼驗證
    • 符号引用驗證
  • 準備:為**類變量(靜态變量)**配置設定記憶體并且設定該類變量的預設初始值
    • 不包含配置設定記憶體給

      final

      修飾的靜态變量(視為常量),因為

      final

      在編譯時就會配置設定,在準備階段會顯示初始化
    • 不會為執行個體變量配置設定記憶體并初始化,因為類變量配置設定在方法區,而執行個體變量随着對象配置設定到堆中
  • 解析:将常量池内的符号引用轉換為直接引用的過程(實際上解析是在JVM執行完初始化後再執行)
    • 符号引用:一組符号用于描述所引用的目标
    // 像#1就是使用到的類的符号引用
    Constant pool:
       #1 = Methodref          #8.#30         // java/lang/Object."<init>":()V
       #2 = Class              #31            // com/atguigu/SellTicket02
       #3 = Methodref          #2.#30         // com/atguigu/SellTicket02."<init>":()V
       #4 = Class              #32            // com/atguigu/SellTicket03
       #5 = Methodref          #4.#30         // com/atguigu/SellTicket03."<init>":()V
       #6 = Methodref          #4.#33         // com/atguigu/SellTicket03.start:()V
       #7 = Class              #34            // com/atguigu/test
               
    • 直接引用:直接指向目标的指針、相對偏移量或間接定位到目标的句柄

3.3 初始化

  • 初始化過程就是執行類構造器方法的過程
    • 該方法不需要定義,是

      javac

      編譯器自動收集類中的所有類變量指派和靜态代碼塊中的語句合并而來
    • 方法中的指令是按語句在源檔案中出現順序執行
    • 如果類中沒有靜态代碼塊或類變量就不會生成該方法
    • 若該類有父類,JVM會保證父類的先執行後再執行子類的
    • JVM必須保證一個類的方法在多線程下被同步加鎖
    Runnable r = () -> {
                System.out.println(Thread.currentThread().getName() + "開始");
                DeadThread deadThread = new DeadThread();
                System.out.println(Thread.currentThread().getName() + "結束");
            };
    Thread thread1 = new Thread(r, "線程1");
    Thread thread2 = new Thread(r, "線程2");
    thread1.start();
    thread2.start();
    // 定義類
    class DeadThread{
        // 用于産生<cinit>方法,死循環用于讓該方法一直執行。如果該方法不是同步加鎖的,則另一個線程也會執行該方法,最終結果會輸出兩個線程都在進行初始化
        static {
            if (true){
                System.out.println(Thread.currentThread().getName() + "進行初始化");
                while (true){}
            }
        }
    }
    // 輸出結果:
    //線程1開始
    //線程2開始
    //線程1進行初始化(此時線程2因為同步加鎖的原因無法執行<cinit>方法)
               
  • 類構造器方法不同于平日提到的類的構造器(在JVM視角下構造器是方法)
tips:
  • 如下面代碼所示,為什麼可以先指派再聲明?在連結中的準備階段,已經對類變量number配置設定記憶體并設定預設值0,然後在初始化階段執行

    clinit

    方法按語句順序先完成靜态代碼塊的指派(即number=5),再初始化為10
static {
    // 靜态語句塊中隻能通路到定義在靜态語句塊之前的變量,定義在它之後的變量,在前面的靜态語句塊中可以指派,但是不能通路
    number = 5;  // 指派不報錯
    System.out.println(number);  // 編譯器會報錯
}
public void m(){
    // 不會報錯,因為執行該方法時,在堆中的對象中已經有了一個名為number的引用并初始化指向了
    System.out.println(number);  
}
private static int number = 10;
// 測試
System.out.println(test2.number);  // 列印10
           

4.加載器的分類

  • 自定義類加載器:所有派生于抽象類

    ClassLoader

    的類加載器(即使用Java語言編寫)都劃分為自定義加載器,圖中的

    Extension Class Loader

    System Class Loader

    因為間接繼承了

    ClassLoader

    ,是以也屬于該類加載器
    • 擴充類加載器

      Extension Class Loader

      • 父類加載器為引導類加載器
      • 如果使用者建立的jar包放在JDK的

        jre/lib/ext

        目錄下,會自動由擴充類加載器加載
    • 系統類加載器(應用程式加載器)

      System Class Loader

      • 父類加載器為擴充類加載器
      • 程式中預設的類加載器,一般來說Java應用的類都是由它完成加載
    • 使用者自定義類加載器:
      • 為什麼要自定義類加載器:
        • 隔離加載類:使用多個架構時可能出現類的沖突
        • 修改類加載的方式
        • 擴充加載源
        • 防止源碼洩露:相當于自定義編譯和反編譯方式
  • 引導類(啟動類)加載器(

    Bootstrap ClassLoader

    )
    • 使用

      C/C++

      實作,嵌套在JVM内部
    • 用于加載Java核心庫(如

      JAVA_HOME/jre/lib/rt.jar

      ),提供JVM自身需要的類
    • 不繼承

      java.lang.ClassLoader

      ,沒有父類加載器
    • 加載擴充類和應用程式類加載器,并指定為它們的父類加載器
    • 處于安全考慮,隻記載包名為

      java、javax、sun

      等開頭的類
JVM記憶體和垃圾回收-02.類加載子系統
// 擷取系統類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// JDK11:[email protected]
// JDK8:sun.misc.Launcher$AppClassLoader@...
System.out.println(systemClassLoader);  
// 擷取系統類加載器的父類加載器 --> 擴充類加載器
// JDK11:[email protected]
// JDK8:sun.misc.Launcher$ExtClassLoader@...
ClassLoader parent = systemClassLoader.getParent();  
System.out.println(parent);
// 擷取擴充類加載器的父類加載器 --> 引導類加載器(擷取不到)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);  // null
// 擷取目前使用者自定義類的加載器(預設使用系統類加載器)
ClassLoader curLoader = ClassLoaderTest.class.getClassLoader();
// JDK11:[email protected]
// JDK8:sun.misc.Launcher$AppClassLoader@...
System.out.println(curLoader);  
// 擷取系統核心類庫的加載器(擷取不到,即使用引導類加載器)
ClassLoader sysloader = String.class.getClassLoader();
System.out.println(sysloader);  // null
           
tips:
  • 上述提到的父類并不是指繼承關系,而是層級上的關系
  • JVM為什麼需要那麼多類加載器?一方面允許在一個JVM裡運作不同的應用程式,另一方面友善使用者獨立的對不同類庫進行運作時增強

5.雙親委派機制

  • 概念:JVM對Class檔案采取的按需加載(即需要使用某個類才将它的Class檔案加載到記憶體生成對象),而加載該類的Class檔案時采用雙親委派模式(即把請求交給父類處理,它是一種任務委派模式)
  • 工作原理:
    • 如果一個類加載器收到了類加載的請求,它并不會自己先去加載,而是把該請求委托給父類加載器去執行
    • 如果父類加載器還存在父類加載器,則繼續向上委托,直到請求到達引導類加載器
    • 如果父類加載器可以完成類的加載,就成功傳回;如果無法完成,子類加載器才去嘗試自己加載
    JVM記憶體和垃圾回收-02.類加載子系統
  • 場景:建立一個

    java.lang

    包,且在該包下寫一個

    String

// src/com/psj/test.java
public static void main(String[] args) {
    String s = new String();
    System.out.println("程式正常執行");
}
// src/java/lang/String
public class String {
    // 如果加載的是該類就會執行該靜态代碼塊
    static {
        System.out.println("執行自定義的String類");
    }
    public static void main(String[] args) {
        System.out.println("程式正常執行");
    }
}
// 執行test.java最終結果還是執行系統類庫的Strig類
// 執行Strng.java會報錯,因為在核心庫中的String類是沒有定義main方法的
// 原因:自定義String類的類加載器預設為系統類加載器,此時由于雙親委派機制,系統類就向上一直找,最終找到引導類加載器。引導類加載器判斷該類處在Java開頭的包下,可以進行加載,但是加載的是核心庫中的String,完成加載後就成功傳回不會再向下執行
           
  • 優點:
    • 避免了類的重複加載
    • 保護程式的安全,防止核心API被随意修改
    • 保護引導類加載器:
    // src/java/lang/Psj.java
    public class Psj {
        public static void main(String[] args) {
            System.out.println("程式正常運作");
        }
    }
    // 最終程式會報錯,即使類名不同,使用的包名為核心庫下的包名依舊不行,防止破壞引導類加載器
               
tips:
  • 加載器能否加載類要看該類是否在指定的包路徑下,比如引導類加載器就會去加載Java開頭的包下的類
  • 上述場景對Java核心源代碼以及引導類加載器的保護稱為沙箱安全機制

6.其他

  • 在JVM中表示兩個Class對象是否為同一個類有兩個必要條件:
    • 類的完整類名必須一緻(包括包名)
    • 加載該類的

      ClassLoader

      必須相同
  • 方法區中會儲存該類是由什麼加載器進行加載的
  • 類的使用分為主動使用和被動使用:
    • 主動使用:
      • 建立類的執行個體
      • 通路通路某個類或接口的靜态變量,或者對該靜态變量指派
      • 調用類的靜态方法
      • 反射
      • 初始化某類的子類
      • JVM啟動時被标記為啟動類的類
    • 被動使用:除了以上情況,其他使用Java類的方式都是對類的被動使用,不會導緻類的初始化