JVM之類載入器ClassLoader

語言: CN / TW / HK

highlight: an-old-hope theme: cyanosis


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第11天,點選檢視活動詳情

類載入器

概述

類載入器負責讀取Java位元組程式碼,並轉換成java.lang.Class類的一個例項的程式碼模組。

類載入器除了用於載入類外,還可用於確定類在Java虛擬機器中的唯一性。

任意一個類,都由載入它的類載入器和這個類本身一同確定其在 Java 虛擬機器中的唯一性,每一個類載入器,都有一個獨立的類名稱空間,而不同類載入器中是允許同名(指全限定名相同)類存在的。

比較兩個類是否“相等”,前提是這兩個類由同一個類載入器載入,否則,即使這兩個類來源於同一個Class 檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,那麼這兩個類就必定不相等。

這裡“相等”是指:類的Class物件的equals()方法、isInstance()方法的返回結果,使用instanceof關鍵字做物件所屬關係判定等情況。

載入器的種類

1.啟動類載入器:Bootstrap ClassLoader

最頂層的載入類,由 C++實現,負責載入%JAVA_HOME%/lib目錄下的jar包和類或者被-Xbootclasspath引數指定的路徑中的所有類。

2.拓展類載入器:Extension ClassLoader

負責載入java平臺中擴充套件功能的一些jar包,如載入%JRE_HOME%/lib/ext目錄下的jar包和類,或-Djava.ext.dirs所指定的路徑下的jar包。

3.系統類載入器/應用程式載入器:App ClassLoader

負責載入當前應用classpath中指定的jar包及-Djava.class.path所指定目錄下的類和jar包。開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

4.自定義類載入器:Custom ClassLoader

通過java.lang.ClassLoader的子類自定義載入class,屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader

驗證不同載入器

每個類載入都有一個父類載入器,可以通過程式來驗證 java public static void main(String[] args) { // App ClassLoader System.out.println(new User().getClass().getClassLoader()); // Ext ClassLoader System.out.println(new User().getClass().getClassLoader().getParent()); // Bootstrap ClassLoader System.out.println(new User().getClass().getClassLoader().getParent().getParent()); // Bootstrap ClassLoader System.out.println(new String().getClass().getClassLoader()); } AppClassLoader的父類載入器為ExtClassLoader, ExtClassLoader的父類載入器為 null,null 並不代表ExtClassLoader沒有父類載入器,而是 BootstrapClassLoader 。 java sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5fdef03a null null

核心方法

檢視類ClassLoader的loadClass方法 ```java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 檢查類是否已經載入 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 父載入器不為空,呼叫父載入器loadClass()方法處理 if (parent != null) { // 讓上一層載入器進行載入 c = parent.loadClass(name, false); } else { // 父載入器為空,使用啟動類載入器 BootstrapClassLoader 載入 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 丟擲異常說明父類載入器無法完成載入請求 // ClassNotFoundException thrown if class not found // from the non-null parent class loader }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 呼叫此類載入器所實現的findClass方法進行載入
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            // resolveClass方法是當位元組碼載入到記憶體後進行連結操作,對檔案格式和位元組碼驗證,併為 static 欄位分配空間並初始化,符號引用轉為直接引用,訪問控制,方法覆蓋等
            resolveClass(c);
        }
        return c;
    }
}

```

JVM類載入機制的三種方式

全盤負責

當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入

注意:

系統類載入器AppClassLoader載入入口類(含有main方法的類)時,會把main方法所依賴的類及引用的類也載入。只是呼叫了ClassLoader.loadClass(name)方法,並沒有真正定義類。真正載入class位元組碼檔案生成Class物件由雙親委派機制完成。

父類委託、雙親委派

父類委託即雙親委派,雙親委派模型是描述類載入器之間的層次關係。它要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。父子關係一般不會以繼承的關係實現,而是以組合關係來複用父載入器的程式碼。

雙親委派模型是指:子類載入器如果沒有載入過該目標類,就先委託父類載入器載入該目標類,只有在父類載入器找不到位元組碼檔案的情況下才從自己的類路徑中查詢並裝載目標類。

雙親委派模型的好處

保證Java程式的穩定執行,避免類的重複載入:JVM區分不同類的方式不僅僅根據類名,相同的類檔案被不同的類載入器載入產生的是兩個不同的類

保證Java核心API不被篡改:如果沒有使用雙親委派模型,而是每個類載入器載入自己的話就會出現一些問題,如編寫一個稱為java.lang.Object 類,程式執行時,系統就會出現多個不同的Object類。反之使用雙親委派模型:無論使用哪個類載入器載入,最終都會委派給最頂端的啟動類載入器載入,從而使得不同載入器載入的Object類都是同一個。

雙親委派機制載入Class的具體過程:

```java 1. ClassLoader先判斷該Class是否已載入,如果已載入,則返回Class物件,如果沒有則委託給父類載入器

  1. 父類載入器判斷是否載入過該Class,如果已載入,則返回Class物件,如果沒有則委託給祖父類載入器

  2. 依此類推,直到始祖類載入器(引用類載入器)

  3. 始祖類載入器判斷是否載入過該Class,如果已載入,則返回Class物件 如果沒有則嘗試從其對應的類路徑下尋找class位元組碼檔案並載入 如果載入成功,則返回Class物件;如果載入失敗,則委託給始祖類載入器的子類載入器

  4. 始祖類載入器的子類載入器嘗試從其對應的類路徑下尋找class位元組碼檔案並載入 如果載入成功,則返回Class物件;如果載入失敗,則委託給始祖類載入器的孫類載入器

  5. 依此類推,直到源ClassLoader

  6. 源ClassLoader嘗試從其對應的類路徑下尋找class位元組碼檔案並載入 如果載入成功,則返回Class物件;如果載入失敗,源ClassLoader不會再委託其子類載入器,而是丟擲異常 ```

注意:

雙親委派機制是Java推薦的機制,並不是強制的機制。可以繼承java.lang.ClassLoader類,實現自己的類載入器。如果想保持雙親委派模型,應該重寫findClass(name)方法;如果想破壞雙親委派模型,可以重寫loadClass(name)方法。

快取機制

快取機制將會保證所有載入過的Class都將在記憶體中快取,當程式中需要使用某個Class時,類載入器先從記憶體的快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。

對於一個類載入器例項來說,相同全名的類只加載一次,即loadClass方法不會被重複呼叫。因此,這就是為什麼修改Class後,必須重啟JVM,程式的修改才會生效的原因。

JDK8使用的是直接記憶體,所以會用到直接記憶體進行快取。因此,類變數為什麼只會被初始化一次的原因。

打破雙親委派

在載入類的時候,會一級一級向上委託,判斷是否已經載入,從自定義類載入器 --> 應用類載入器 --> 擴充套件類載入器 --> 啟動類載入器,如果到最後都沒有載入這個類,則回去載入自己的類。

在這裡插入圖片描述

雙親委派模型並不是強制模型,而且會帶來一些些的問題。例如:java.sql.Driver類,JDK只能提供一個規範介面,而不能提供實現。提供實現的是實際的資料庫提供商,提供商的庫不可能放JDK目錄裡。

重寫loadclass方法

自定義類載入,重寫loadclass方法,即可破壞雙親委派機制

因為雙親委派的機制都是通過這個方法實現的,這個方法可以指定類通過什麼類載入器來進行載入,所有如果改寫載入規則,相當於打破雙親委派機制

```java import cn.ybzy.demo.Test;

import java.io.*;

public class MyClassLoader extends ClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData;
    try {
        classData = loadClassData(name);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    if (classData == null) {
        throw new ClassNotFoundException();
    } else {
        return defineClass(name, classData, 0, classData.length);
    }
}

private byte[] loadClassData(String className) throws IOException {
    String replace = className.replace('.', File.separatorChar);
    String path = ClassLoader.getSystemResource("").getPath() + replace + ".class";
    InputStream inputStream = null;
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
        inputStream = new FileInputStream(path);
        byteArrayOutputStream = new ByteArrayOutputStream();
        int bufferSize = 1024;
        byte[] buffer = new byte[bufferSize];
        int length = 0;
        while ((length = inputStream.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, length);
        }
        return byteArrayOutputStream.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (byteArrayOutputStream != null) {
            byteArrayOutputStream.close();
        }
        if (inputStream != null) {
            inputStream.close();
        }
    }

    return null;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 修改classloader的原雙親委派邏輯,從而打破雙親委派
                if (name.startsWith("cn.ybzy.demo")) {
                    c = findClass(name);
                } else {
                    c = this.getParent().loadClass(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

} java public static void main(String[] args) throws ClassNotFoundException { MyClassLoader classLoader = new MyClassLoader(); Class<?> aClass = classLoader.loadClass(Test.class.getName()); System.out.println(aClass.getClassLoader()); } ```

java cn.ybzy.demo.MyClassLoader@2f410acf

自定義類載入器

自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在類中對檔案進行解密。

準備位元組碼檔案

建立Test類,同時進行javac Test.class編譯成位元組碼檔案,放到目錄下:D:\Temp\cn\ybzy\demo ```java package cn.ybzy.demo;

public class Test { public static void main(String[] args) { System.out.println("Test..."); } } ```

建立自定義類載入器

```java import java.io.*;

public class MyClassLoader extends ClassLoader { private String root;

protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData;
    try {
        classData = loadClassData(name);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    if (classData == null) {
        throw new ClassNotFoundException();
    } else {
        return defineClass(name, classData, 0, classData.length);
    }
}

private byte[] loadClassData(String className) throws IOException {
    String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    InputStream inputStream = null;
    ByteArrayOutputStream byteArrayOutputStream = null;
    try {
        inputStream = new FileInputStream(fileName);
        byteArrayOutputStream = new ByteArrayOutputStream();
        int bufferSize = 1024;
        byte[] buffer = new byte[bufferSize];
        int length = 0;
        while ((length = inputStream.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, length);
        }
        return byteArrayOutputStream.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (byteArrayOutputStream != null) {
            byteArrayOutputStream.close();
        }
        if (inputStream != null) {
            inputStream.close();
        }
    }

    return null;
}

public String getRoot() {
    return root;
}

public void setRoot(String root) {
    this.root = root;
}

} ```

執行測試

啟動main方法,執行測試

java public static void main(String[] args) { MyClassLoader classLoader = new MyClassLoader(); classLoader.setRoot("D:\\Temp"); Class<?> testClass = null; try { testClass = classLoader.loadClass("cn.ybzy.demo.Test"); Object object = testClass.newInstance(); System.out.println(object.getClass().getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }

java cn.ybzy.demo.MyClassLoader@5679c6c6 將Test類放到專案類路徑下,由於雙親委託機制的存在,會直接導致該類由 AppClassLoader 載入,而不會通過自定義類載入器來載入 在這裡插入圖片描述 java sun.misc.Launcher$AppClassLoader@18b4aac2

注意事項

```java 1、這裡傳遞檔名需要是類的全限定性名稱,因為defineClass方法是按這種方式/格式進行處理 因此,若沒有全限定名,需要將類的全路徑載入進去

2、不要重寫loadClass方法,因為這樣容易破壞雙親委託模式

3、Test類本身可以被AppClassLoader類載入,因此不能把Test.class放在類路徑下 否則,由於雙親委託機制的存在,會直接導致該類由AppClassLoader載入,而不會通過自定義類載入器來載入 ```