JVM第二篇-類載入子系統

欽拆大仁發表於2020-11-25

1、類載入過程

JVM架構圖

1.1類載入子系統

類載入子系統負責從檔案系統或者網路中載入Class檔案,class檔案在檔案開頭有特定的檔案標識。
ClassLoader只負責class檔案的載入,至於它是否可以執行,則由Execution Engine決定。
載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體對映)

 

1.2類載入器ClassLoader角色

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

 

1.3類的載入過程

  • 通過一個類的全限定名獲取定義此類的二進位制位元組流
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

載入

  • 從本地系統直接載入
  • 通過網路獲取,典型場景:Web Applet
  • 從zip壓縮包中讀取,稱為日後jar、war格式的基礎
  • 執行時計算生成,使用做多的是:動態代理技術
  • 由其他檔案生成,典型場景:JSP應用
  • 從專有資料庫中提取.class檔案,比較少見
  • 從加密檔案中獲取,典型的防Class檔案被反編譯的保護措施

連結

驗證(Verify)

  • 目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,保證被載入類的正確性,不會危害虛擬機器自身安全
  • 主要包含四種驗證:檔案格式驗證、後設資料驗證、位元組碼驗證、符號引用驗證

準備(Prepare)

  • 為類分配記憶體並且設定該類變數的預設初始值,即零值
  • 這裡不包含用final修飾的static,因為final在編譯的時侯就會分配了,準備階段會顯式初始化
  • 這裡不會為例項變數分配初始化,類變數會分配在方法區,而例項變數是會隨著物件一起分配到Java堆中

解析(Resolve)

  • 將常量池內的符號引用轉換為直接引用的過程
  • 事實上,解析操作就是一組符號來描述所引用的目標,符號引用的字面量形式明確定義在《Java虛擬機器規範》的Class檔案格式中。直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼
  • 解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對應常量池中的
    CONSTANT_Class_info、CONSTANT_Fileder_info、CONSTANT_Methodref_info

初始化

  • 初始化階段就是執行類構造器方法<clinit>()的過程
  • 此方法不需定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來
  • 構造器方法中指令按語句在原始檔中出現的順序執行

 

2、類載入器分類

2.1虛擬機器支援的類載入器

  • JVM支援兩種型別的類載入器,分別是引導類載入器(Bootstrap ClassLoader)自定義類載入器(User-Defined ClassLoader)
  • 從某概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類載入器,但是Java虛擬機器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器

public class ClassLoaderTest {
    public static void main(String[] args) {

        //獲取系統類載入器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //獲取其上層,擴充套件類載入器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1b6d3586

        //獲取上上層: 獲取不到引導類載入器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //使用者自定義類來說: 使用系統類載入器進行載入
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String類使用引導類載入器進行載入的 ---> Java的核心類庫都是使用引導類進行載入的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    }
}

2.2虛擬機器自帶的載入器

  • 啟動類載入器(引導類載入器,Bootstrap ClassLoader)

    • 這個類載入使用C/C++語言實現的,巢狀在JVM內部
    • 它用來載入Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resource.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/lib/ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的JAR放在此目錄下,也會自動由擴充套件類載入器載入
  • 應用程式類載入器(系統類載入器,AppClassLoader)

    • Java語言編寫,由sun.misc.Lancher$AppClassLoader實現
    • 派生於ClassLoader類
    • 父類載入器為擴充套件類載入器
    • 它負責載入環境變數classpath或系統屬性 java.class.path 指定路徑下的類庫
    • 該類載入是程式中預設的類載入器,一般來說,Java應用的類都是由它來完成載入
    • 通過ClassLoader#getSystemClassLoader()方法可以獲取到該類載入器
public class ClassLoaderTest1 {
    public static void main(String[] args) {
        System.out.println("**********啟動類載入器*********");
        //獲取BootstrapClassLoader能夠載入的API的路徑
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urls) {
            System.out.println(element.toExternalForm());
        }
        //從上面的路徑中隨意選擇一個類,來看看他的類載入器是什麼: 引導類載入器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);


        System.out.println("**********擴充套件類載入器**********");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }
        //從上面的路徑中隨意選擇一個類,來看看他的類載入器是什麼: 擴充套件類載入器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4b67cf4d
    }
}

2.3使用者自定義類載入器

在Java的日常應用程式開發中,類的載入幾乎是由上述3類載入器相互配合執行的,在必要時,我們還可以自定義類載入器,來定製類的載入方式

  • 為什麼要自定義類載入器?

    • 隔離載入類
    • 修改類載入的方式
    • 擴充套件載入源
    • 防止原始碼洩露
  • 使用者自定義類載入器實現步驟:

    • 開發人員可以通過繼承抽象類java.lang.ClassLoader類的方式,實現自己的類載入器,以滿足一些特殊的需求
    • 在JDK1.2之前,在自定義類載入器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類載入器,但是JDK1.2之後已不再建議使用者去覆蓋loadClass()方法,而是建議把自定義的類載入邏輯寫在findClass()方法中
    • 在編寫自定義類載入器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫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 {
                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();
        }
    }
}

2.4ClassLoader的使用說明

Classloader類,它是一個抽象類,氣候所有的類載入器都繼承自ClassLoader(不包括啟動類載入器)

方法名稱(都不是抽象方法)描述
getParent()返回該類載入器的超類載入器
loadClass(String name)載入名稱為name的類,返回結果為java.lang.Class類的例項
findClass(String name)查詢名稱為name的類,返回結果為java.lang.Class類的例項
fingLoaderClass(String name)查詢名稱為name的已經被載入過的類,返回結果為java.lang.Class類的例項
defineClass(String name,byte[] b,int off,int len)把位元組陣列b中的內容轉換為一個Java類,返回結果為java.lang.Class類的例項
resolveClass(Class<?>c)連線指定的一個Java類

public class ClassLoaderTest2 {

    public static void main(String[] args) {
        try {
            //1.
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);
            //2.
            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
            System.out.println(classLoader1);
            //3.
            ClassLoader classLoader2 = classLoader.getSystemClassLoader().getParent();
            System.out.println(classLoader2);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

3、雙親委派機制

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

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

自定義類:java.lang.String

自定義類:java.lang.codenow(java.lang包需要訪問許可權,阻止我們用包名自定義類)

沙箱安全機制

自定義String類,但是在載入自定義String類的時候會率先使用引導類載入器載入,而引導類載入器在載入的過程中會先載入jdk自帶的檔案(rt.jar包中javalangString.class),報錯資訊說沒有main方法,就是因為載入的是rt.jar包中的String類。這樣可以保證對Java核心原始碼的保護,這就是沙箱安全機制。(360沙箱虛擬環境)

其他

在JVM中表示兩個class物件是否為同一個類存在兩個必要條件:

類的完整性名必須一致,包括包名。

載入這個類的ClassLoader(指ClassLoader例項物件)必須相同。

換句話說,在JVM中,即使這兩個類物件(class物件)來源同一個Class檔案,被同一個虛擬機器所載入,但只要載入他們的ClassLoader例項物件不同,那麼這兩個類物件也是不相等的。

 

對類載入器的引用

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

類的主動使用和被動使用

Java對類的使用方式分為:主動使用和被動使用
主動使用和被動使用的區別就是初始化階段是否執行

主動使用,又分為七種情況

  • 建立類的例項
  • 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  • 呼叫類的靜態方法
  • 反射(比如:Class.forName("top.codenow.jvm.xxx"))
  • 初始化一個類的子類
  • Java虛擬機器啟動時被標明為啟動類的類
  • JDK 7 開始提供的動態語言支援:
    java.lang.invoke.MethodHandle例項的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic控制程式碼對應的類沒有初始化,則初始化
  • 除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。

 

 

 

 

 

相關文章