1. 程式人生 > >類載入器深入理解和雙親委託模型的案例分析

類載入器深入理解和雙親委託模型的案例分析

類載入器深入理解和雙親委託模型的案例分析

  • 我們知道類必須通過類載入器載入後,我們程式才可以使用。接下來我們就對類載入器進行分析,Java虛擬機器的類載入器是如何載入類的。首先我們可以從ClassLoader的原始碼分析入手。

ClassLoader 的原始碼分析

ClassLoader 的javadoc文件

  • javadoc文件是最權威的官方講解,可以對ClassLoader有一個比較全面且正確的一個認知。下面是javadoc內容。

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
翻譯: 一個載入器就是一個物件負責載入類。ClassLoader 是抽象類,給定了一個二進位制名字(可以簡單的理解為一個字串類似: "java.lang.String",就程式碼一個String類的二進位制名稱),ClassLoader應該嘗試定位或者生成構成類定義的相應的資料。(定位說明類的相關資料已經存在例如String這個類,我們程式設計師已經編寫好的類等,生成對應的是沒有類的相關資料,需要classLoader進行生成,因為java一些類需要執行期動態生成出來的,例如動態代理,在執行期之前是不存在的),一種典型的策略就是將一個二進位制的名字轉換成一個檔名字。然後從檔案系統中去讀取這個檔案中所包含的位元組的class檔案,我們就是從磁碟上去讀取class位元組碼檔案。這個是比較典型的,還可以從其他地方讀取網路中,資料庫等。

Every Class object contains a reference to the ClassLoader that defined it.
翻譯:每個class物件都會包含定義這個class的ClassLoader的引用。因為class類都會有一個getClassLoader這個方法。換句話說class會含有載入該class物件的ClassLoader物件。

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
翻譯: 對於陣列類的class物件,並不是有ClassLoader進行建立的,而是由java執行時按需要進行自動建立的。對於陣列類的getclassloader()返回的陣列類的類裝入器與其元素型別的類裝入器相同;如果元素型別是基本型別,則陣列類沒有類裝入器。
程式碼案例如下所示:

public static void main(String[] args) {
        // String 是rt.jar 是根類載入器負責載入的(後面會將各個載入器的載入範圍) 通過getClassLoader可知 根類載入器返回為Null
        String[] strings = new String[2];
        System.out.println(strings.getClass().getClassLoader());
        // Mytest08是程式設計師自己編寫的 系統類載入器進行載入 返回為: 系統類載入器
        Mytest08[] mytest08s = new Mytest08[2];
        System.out.println(mytest08s.getClass().getClassLoader());
        // int由於int是基本型別 所以返回為null
        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());

    }

Applications implement subclasses of ClassLoader in order to extend the manner in which the Java virtual machine dynamically loads classes.
Class loaders may typically be used by security managers to indicate security domains.
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
翻譯:ClassLoader類使用委託模型來搜尋類和資源。類裝入器的每個例項都有一個關聯的父類裝入器。當請求查詢類或資源時,類載入器例項將把對類或資源的搜尋委託給其父類載入器,然後再嘗試查詢類或資源本身。虛擬機器的內建類裝入器稱為“啟動類載入器”,它本身沒有父類載入器,但可以作為類載入器例項的父類。
這個就介紹了雙親委派機制的原理。可參考如下所示的圖:

我們還可以通過程式碼進行驗證java虛擬機器載入器的層次結構如下所示:

public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        /**
         * 列印載入器的層次結構
         * sun.misc.Launcher$AppClassLoader@18b4aac2 系統類載入器
         * sun.misc.Launcher$ExtClassLoader@610455d6 擴充套件類載入器
         * null 根類載入器
         */
        while (systemClassLoader != null) {
            //返回父類載入器
            // 根類載入器 返回為Null
            systemClassLoader = systemClassLoader.getParent();
            System.out.println(systemClassLoader);
        }
    }

Class loaders that support concurrent loading of classes are known as parallel capable class loaders and are required to register themselves at their class initialization time by invoking the ClassLoader.registerAsParallelCapable method. Note that the ClassLoader class is registered as parallel capable by default. However, its subclasses still need to register themselves if they are parallel capable. In environments in which the delegation model is not strictly hierarchical, class loaders need to be parallel capable, otherwise class loading can lead to deadlocks because the loader lock is held for the duration of the class loading process (see loadClass methods).
翻譯:支援類的併發載入的類載入器稱為支援並行的類載入器,需要通過呼叫類載入器在類初始化時註冊它們自己。registerAsParallelCapable方法。注意,預設情況下ClassLoader類被註冊為支援並行的。但是,它的子類仍然需要註冊它們自己,如果它們是並行的。在委託模型沒有嚴格層次結構的環境中,類裝入器需要具有並行能力,否則類裝入可能會導致死鎖,因為裝入器鎖在類裝入過程期間一直持有(請參閱loadClass方法)。

Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. For example, on UNIX systems, the virtual machine loads classes from the directory defined by the CLASSPATH environment variable.
翻譯:通常,Java虛擬機器以平臺相關的方式從本地檔案系統載入類。例如,在UNIX系統上,虛擬機器從CLASSPATH環境變數定義的目錄載入類。 換句話說:系統類載入器通過我們系統配置的classpath目錄來載入我們專案中的我們程式設計師編寫的class問題

However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.
翻譯:但是,有些類可能不是起源於檔案;它們可能來自其他來源,例如網路,也可能由應用程式構造(動態代理)。方法defineClass將位元組陣列轉換為類的例項。可以使用class . newinstance建立這個新定義的類的例項。

The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.
翻譯: 類裝入器建立的物件的方法和建構函式可以引用其他類。為了確定引用的類,Java虛擬機器呼叫最初建立類的類裝入器的loadClass方法。

For example, an application could create a network class loader to download class files from a server. Sample code might look like:
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .

The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
上面這兩個demo就是展示類載入器載入類的具體案例。

自定義寫一個使用者類載入器

  • 程式碼如下所示:
public class MyTest11 extends ClassLoader {
    // 指定載入器的載入路徑
    private String path;
    private String classsLoaderName;
    //檔案的副檔名
    private final String fileExtension = ".class";


    public MyTest11(String classsLoaderName) {
        // 指定父載入器 預設系統類載入器 具體可以看原始碼在這裡就不做過多的介紹了
        super();
        this.classsLoaderName = classsLoaderName;
    }

    /**
     * 顯示的指定父類classloader 可以將使用者自定義的loader1 作為 使用者自定義loader2 的父載入器 提供了很多的擴充套件性
     */
    public MyTest11(ClassLoader parent, String classsLoaderName) {
        super(parent);
        this.classsLoaderName = classsLoaderName;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 自定義的classloader 自需要重寫findCLass 這個類 findClass 類的作用就是通過一個二進位制的類名去載入一個class物件
        // 
        System.out.println("findclass invoke" + name);
        System.out.println("class loader name" + this.classsLoaderName);
        byte[] data = loadClassData(name);
        // defineClass 是將一個byte[] 陣列轉換成一個class 物件
        return this.defineClass(name, data, 0, data.length);
    }


    /**
     * 自定義根據二進位制類名 載入類的屬性以位元組陣列的形式返回 具體實現就是io操作
     * @param name 二進位制類名
     * @return byte[]
     */
    private byte[] loadClassData(String name) {
        InputStream inputStream = null;
        byte[] data = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        // 匹配路徑 mac '/'  win '\\'
        name = name.replace(".", "/");
        try {
            inputStream = new FileInputStream(new File(this.path + name + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }
            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
                byteArrayOutputStream.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return data;
    }

    /**
     * 設定類的載入路徑
     */
    public void setPath(String path) {
        this.path = path;
    }

Coding驗證類載入器的機制

  • 案例01
public static void main(String[] args) throws Exception {
        //這裡我們呼叫了loadClass 方法內部呼叫了我們重寫的findClass方法
        // MyTestBean 是我程式定義的類
        // Users/panda/Documents/SourceCode/jvm_lecture/target/classes 專案中的class檔案目錄

        MyTest11 myTest11 = new MyTest11("myLoader");
        myTest11.setPath("/Users/panda/Documents/SourceCode/jvm_lecture/target/classes");
        Class<?> aClass = myTest11.loadClass("com.study.jvm.day11.MyTestBean");
        System.out.println("aClass = " + aClass.hashCode());
        Object object = aClass.newInstance();
        System.out.println("object = " + object);
        System.out.println("aClass 的載入器為:" + aClass.getClassLoader());
    }
  • 輸出結果:

aClass = 1627674070
object = com.study.jvm.day11.MyTestBean@511d50c0
aClass 的載入器為:sun.misc.Launcher$AppClassLoader@18b4aac2

  • 結果分析

從輸出結果上來看,並沒有列印我們 findclass invoke這句話,說明沒有走我們自定義的ClassLoader。因為我們自定義的myLoader準備載入前會先委託給父類載入器系統類載入器進行載入,系統類載入器可以載入(classpath下)就有MyTestBean.class。然後系統類載入器進行載入,而類只會被載入一次。所以沒有走我們自定義的classLoader。

  • 案例02
    public static void main(String[] args) throws Exception {
        //這裡我們呼叫了loadClass 方法內部呼叫了我們重寫的findClass方法
        // MyTestBean 是我程式定義的類
        /**
         * 需要注意的點:
         * 01 首先將本專案中的MyTestBean.class 從本專案轉移到自定義的一個路徑  本案例中我把它轉移到桌面
         * 02 刪除本專案的MyTestBean.class 不刪除我們的系統類載入器根據二進位制類名也會載入該class
         */

        MyTest11 myTest11 = new MyTest11("myLoader");
        myTest11.setPath("/Users/panda/Desktop/");
        Class<?> aClass = myTest11.loadClass("com.study.jvm.day11.MyTestBean");
        System.out.println("aClass = " + aClass.hashCode());
        Object object = aClass.newInstance();
        System.out.println("object = " + object);
        System.out.println("aClass 的載入器為:" + aClass.getClassLoader());
    }
  • 輸出結果:

findclass invoke: com.study.jvm.day11.MyTestBean
class loader name: myLoader
aClass = 1625635731
object = com.study.jvm.day11.MyTestBean@5e2de80c
aClass 的載入器為:com.study.jvm.day11.MyTest11@610455d6

  • 結果分析

從輸出的結果可以看到MyTestBean是由我們自定義的MyTest11載入的,因為在載入的時候MyTest11會將委託任務交給父載入器們,他們都載入不了,然後就會自己去載入。

結束語

  • ClassLoader裡面還有很多有用的方法,小夥伴們可以藉助文中的案例慢慢手動體會。