JVM上篇:類載入子系統

JVM類載入

1.記憶體結構概述

  1. 類載入器子系統
    1. Loading階段,載入class文件
    2. Linking階段
      1. 驗證
      2. 準備
      3. 解析
    3. Initialization階段,初始化
  2. 運行時數據區
    1. 方法區,把需要引用的類的位元組碼文件都會載入到方法區,存放類的資訊,方法資訊等等
    2. 堆(heap)區,創建java對象的主體都分配到堆空間中
    3. 虛擬機棧,每個執行緒一份
    4. pc暫存器(程式計數器),每個執行緒一份
    5. 本地方法棧,本地方法介面調用,使用本地方法棧
  3. 執行引擎
    1. 解釋器
    2. 即時編譯器
    3. 垃圾回收期

image-20220222215653975

2.類載入子系統概述

  1. 類載入器子系統的作用

    ClassLoader負責從文件系統或者從網路中載入Class文件,至於它是否可以運行,有Execution Engine來決定。

  2. 載入類到哪裡?

    載入的類的資訊存放在記憶體空間的「方法區」,除了類的資訊外,方法區還會存放運行時的常量的映射等資訊。

3.類的載入過程

2.1載入

  1. 通過類的全類名獲取Class文件的二進位流
  2. 將載入進來的位元組流轉化為方法區運行時的數據結構
  3. 在記憶體中生成一個這個類的對象Class對象,作為方法區這個類的各種數據的訪問入口

2.2Linking

2.2.1驗證(Verify)
  1. 確保Class文件的位元組流中的資訊正確,保證類載入的正確性
  2. 主要包括四種驗證:文件格式驗證,元數據驗證,位元組碼驗證,符號引用驗證
  3. 例如下面一個元數據的驗證,java位元組碼文件初始文件都是CA FE BA BE

image-20220216231806385

2.2.2準備(Prepare)
  1. 為類變數分配記憶體,並設置類變數的默認初始值,即零值
  2. final修飾的常量,在編譯的時候就會分配值了
  3. 不會對實例變數進行初始化,類變數會分配在方法區中,實例變數會隨著對象一起分配到堆空間中
2.2.3解析(Resolve)
  1. 將常量池內的符號引用,轉化為直接引用。

    例:Object類的引用

2.3初始化(Initlization)

  1. 初始化階段就是執行類構造器方法<clinit>()的過程,他和類的構造器<init>()是不一樣的。

    此方法是javac編譯器自動收集類中的所有靜態變數的賦值動作和靜態程式碼塊中的語句合併而來

  2. 執行順序按照編寫的程式碼的順序執行

  3. <init>為類的構造器方法,任何一個類聲明以後,內部至少存在一個類的構造器

  4. 若被載入的類具有父類,JVM會保證子類的<clinit>()執行之前,父類的<clinit>()已經執行完畢

舉例:

  1. 定義父類

    public class initTest {
            public static int A = 1;
            static {
                A=2;
            }
    }
    
  2. 定義子類

    public class Son extends initTest {
        public static int B =A;
        public static void main(String[] args) {
            System.out.println(B);
        }
    }
    
  3. 編譯運行

    image-20220217222629431

  4. 查看位元組碼文件,可以看到載入B的時候先調了父類的clinit,載入了A

    image-20220217222811967

  5. 虛擬機必須保證一個類的<clinit>()方法在多執行緒的情況下被同步加鎖

舉例:

  1. 編寫被調用的class

    public class initTest {
        static {
            //這裡使用循環,目的是為了卡住<clinit>()方法,讓別的執行緒等待
            if (true){
                System.out.println(Thread.currentThread().getName()+"進來了");
                while (true){
                }
            }
        }
    }
    
  2. 編寫兩個執行緒,兩個執行緒都載入initTest,因為對於JVM來說,同一個類只會被載入一次,載入以後類資訊等存放在方法區中

    public class Son {
        public static void main(String[] args) {
            Runnable r = () ->{
                System.out.println(Thread.currentThread().getName()+"開始");
                initTest initTest = new initTest();
                System.out.println(Thread.currentThread().getName()+"結束");
            };
    
            Thread t1 = new Thread(r,"執行緒1");
            Thread t2 = new Thread(r,"執行緒2");
    
            t1.start();
            t2.start();
        }
    }
    
  3. 編譯運行,第一個載入initTest的執行緒會進去,而另一個載入initTest的執行緒則會在後面等待

    image-20220217225306944

4.類載入器的分類

JVM支援兩種類型的類載入器,分別為引導類載入器(Bootstrap ClassLoader),和自定義類載入器(User-Defined ClassLoader)

JVM將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器

可能你會疑惑,拓展類載入器,和系統類載入器是什麼類型,其實他們都是派生於ClassLoader類的,JVM均視為自定義類載入器

引導類載入器,拓展類載入器,系統類載入器,用戶自定義載入器,這四者的關係是包含關係。不是子父繼承的關係

3.1虛擬機自帶的載入器

3.1.1引導類載入器(Bootstrap ClassLoader)
  1. 這個類載入器使用c/c++實現,嵌套與JVM內部
  2. 它用來載入Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resource.jar或者 sun.boot.class.path 路徑下的內容),用於提供JVM自身需要的類
  3. 它並不繼承與java.lang.ClassLoader,c語言編寫,無父載入器
  4. 它用來載入拓展類載入器和系統類載入器(應用程式類載入器),並指定Bootstrap為他們的父載入器
  5. 出於安全考慮,Bootstrap ClassLoader只載入包名為java,javax,sun等開頭的類
3.1.2拓展類載入器(Extension ClassLoader)
  1. java語言編寫,為sun.misc.Launcher的一個內部類
  2. 派生於ClassLoader類
  3. 父載入器為引導類載入器
  4. 從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK安裝目錄的jre/lib/ext子目錄下載入類庫,如果用戶創建的JAR放在此目錄下,也會由拓展類載入器自動載入。
3.1.3系統類載入器(應用程式類載入器 AppClassLoader)
  1. java語言編寫,為sun.misc.Launcher的內部類
  2. 派生於ClassLoader類
  3. 父類載入器為拓展類載入器
  4. 它負責載入環境遍歷classpath或者是系統屬性java.class.path指定路徑下的類庫
  5. 該類載入器是程式中默認的類載入器,一般來說,我們編寫的Java應用的類都是由它來完成載入
  6. 通過ClassLoader#getSystemClassLoader()的方法,可以獲取到該類載入器

舉例:

import sun.misc.Launcher;

import java.net.URL;
import java.security.Provider;

public class ClassLoaderTestFyp {
    public static void main(String[] args) {
//        獲取引導類載入器載入的路徑
        System.out.println("====================引導類載入器載入的路徑==========================");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL.toExternalForm());
        }
        //從上面的路徑中隨意選擇一個類,看一下這個類的類載入器是什麼
        System.out.println("====================獲取引導類載入器===============================");
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader); //應該為null,因為引導類載入器是c和c++編寫,我們無法獲取到

//        拓展類載入器
        System.out.println("====================拓展類載入器載入的路徑==========================");
        String extDirs = System.getProperty("java.ext.dirs");
        System.out.println(extDirs);

//        系統類載入器classpath
        System.out.println("====================系統類載入器載入的路徑==========================");
        String classpath = System.getProperty("java.class.path");
        for (String s : classpath.split(";")) {
            System.out.println(s);
        }
    }
}
3.1.4用戶自定義類載入器

為什麼要自定義類的載入器:

  1. 隔離載入類,(主要解決不同版本jar包的版本衝突)
  2. 修改類載入的方式
  3. 擴展載入源
  4. 防止源碼泄露

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

  1. 繼承java.lang.ClassLoader抽象類
  2. 在jdk1.2之後,建議把自定義的類載入器邏輯寫在findclass當中
  3. 如果沒有太過於複雜的需求,可以繼承URLClassLoader,可以避免自己去讀取位元組碼流的方式,避免自己寫URLClassLoader

5.ClassLoader介紹

ClassLoader是一個抽象類,其後所有的類載入器都繼承自ClassLoader(除引導類載入器)

img

獲取ClassLoader的途徑
  • 方式一:獲取當前ClassLoader
clazz.getClassLoader()
  • 方式二:獲取當前執行緒上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
  • 方式三:獲取系統的ClassLoader
ClassLoader.getSystemClassLoader()
  • 方式四:獲取調用者的ClassLoader
DriverManager.getCallerClassLoader()

6.雙親委派機制

Java虛擬機對class文件採用的按需載入的方式,也就是說當需要使用此類的時候,才會把這個類的class文件載入到記憶體,生成class對象。且,在載入某個類的class文件的時候,Java虛擬機採用的是雙親委派模式,即把請求交給父類載入器處理,它是一種任務委派模式。

工作原理
  1. 如果一個類載入器收到了類載入的請求,它並不會先自己去載入這個類,而是把這個請求委託給父類的載入器去執行
  2. 如果父類載入器還存在其父類載入器,則依次向上委託,請求最終將到達頂層的引導類載入器
  3. 如果父類載入器可以完成類載入的任務,則就成功返回,如果父類載入器無法完成類載入的任務,則子載入器才會自己嘗試去載入類,這就是雙親委派模式。

image-20220222131735350

舉例:

當我們載入jdbc.jar包用於實現資料庫連接的時候,首先我們要知道的是jdbc.jar是基於SPI介面進行實現的,所以在載入的時候會進行雙親委派,從引導類載入器載入SPI核心的類,然後再載入SPI介面類,接著在進行反向委派,通過執行緒上下文類載入器進行實現類jdbc.jar的載入。

image-20220222212254372

優勢
  1. 避免類的重複載入

  2. 保護程式安全,防止核心的API被隨意的修改

    舉例:假設我們自己實現了一個java.lang.String,如果給這個類寫個main方法,去運行,是不行的,因為當類載入器收到類載入請求的時候會向上委託,會載入核心的String類,而核心的String類無此方法,所以會報錯為方法找不到,這也稱為沙箱安全機制。

7.其他

  1. 如何判斷兩個Class對象是否相等

    在JVM中表示兩個Class對象是否為同一個類,有兩個必要的條件

    • 類的全類名必須相同
    • 載入這個類的ClassLoader必須相同

    也就是說,即使兩個類對象來自同一個class文件,但是載入他們的類載入器不同,那這兩個對象也是不想等的。

8.類的主動使用和被動使用

java程式對類的使用方式分為:主動使用和被動使用

主動使用,又分為7種情況:

  • 創建類的實例
  • 訪問某個類或介面的靜態變數,或者對這個靜態變數賦值。
  • 調用類的靜態方法
  • 反射
  • 初始化一個類的子類
  • Java虛擬機啟動的時候被標明為啟動類的類
  • JDK 7 開始提供的動態語言的支援

除了以上7種情況,其他使用java類的方式,都看作是對類的被動使用,不會導致類的初始化。