JVM--4.通過demo分析類載入器理論

2020-08-12 10:31:11

1. 使用類載入器載入類的過程

通過ClassLoader原始碼分析 ClassLoader.loadClass()方法得知載入類有以下三個過程。

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 {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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();
                    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;
        }
    }

1.1 Class<?> c = findLoadedClass(name);

如果Java虛擬機器已將此載入程式記錄爲具有該二進制名稱的類的初始載入程式,則返回具有給定二進制名稱的類。

1.2 parent.loadClass(name, false);

這裏就體現出雙親委託是如何實現的,不管是什麼類載入器載入,會先拋給parent類載入器嘗試載入,

1.3 c = findClass(name);

如果從下致上都不無法載入該類,則將從上致下開始嘗試載入,如果到最底層都無法載入,則拋出異常,類找不到。

2. 自定義類載入器

所有的自定義類載入器都需要 extends ClassLoader,一般只需要重寫 findClass()方法即可,在方法中,根據指定檔名在指定的路徑下獲取檔案流資訊,然後交於jvm底層 defineClass()方法處理即可。

以下是自定義的類載入器

package jvmtest;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.text.MessageFormat;
import java.util.Optional;

class CustomClassLoader extends ClassLoader {


    /**
     * 類載入器名稱
     */
    private String classLoaderName;

    /**
     * 類載入器根目錄
     */
    private String path;

    private static final String suffixName = ".class";

    /**
     * 預設以systemClassLoader爲父類別載入器
     *
     * @param classLoaderName
     */
    public CustomClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    /**
     * 使用傳入的classlaoder作爲其雙親
     *
     * @param parent
     * @param classLoaderName
     */
    public CustomClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }


    /**
     * Finds the class with the specified <a href="#name">binary name</a>.
     * This method should be overridden by class loader implementations that
     * follow the delegation model for loading classes, and will be invoked by
     * the {@link #loadClass <tt>loadClass</tt>} method after checking the
     * parent class loader for the requested class.  The default implementation
     * throws a <tt>ClassNotFoundException</tt>.
     *
     * @param name The <a href="#name">binary name</a> of the class
     * @return The resulting <tt>Class</tt> object
     * @throws ClassNotFoundException If the class could not be found
     * @since 1.2
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("findClass..");
        byte[] bytes = this.loadClassData(name);
        return this.defineClass(name, bytes, 0, bytes.length);
    }


    /**
     * 載入指定className對應的檔案,返回對應的數據,這裏注意使用 read()讀取class檔案內容時,只能一個一個位元組的讀取,不能一次多個。
     *
     * @param className
     * @return
     */
    private byte[] loadClassData(String className) {
        className = className.replace(".", "/");
        byte[] readDatas = null;
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             FileInputStream fileInputStream = new FileInputStream(new File(Optional.ofNullable(this.path).orElse("") + className + suffixName));) {

            int result;
            while ((result = fileInputStream.read()) != -1) {
                byteArrayOutputStream.write(result);
            }

            /*byte[] datas = new byte[1];
            while (fileInputStream.read(datas) != -1) {
                byteArrayOutputStream.write(datas);
            }*/
            readDatas = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return readDatas;
    }

    @Override
    public String toString() {
        return MessageFormat.format("[{0}]", this.classLoaderName);
    }

    public void setPath(String path) {
        this.path = path;
    }
}

測試類

package jvmtest;

/**
 * @author ztkj-hzb
 * @Date 2019/12/13 14:48
 * @Description
 */
public class Test {


    public static void main(String[] args) throws ClassNotFoundException {

        CustomClassLoader classLoader = new CustomClassLoader("customClassLoader");
        Class<?> loadClass = classLoader.loadClass("jvmtest.Test1");
        System.out.println(loadClass.getClassLoader());


    }

}

執行測試類,會看到一下結果

sun.misc.Launcher$AppClassLoader@18b4aac2

從結果上看,載入該類的類載入器並不是我們寫的自定義的類載入器,而是系統類載入器。因爲程式碼裡載入的類 「jvmtest.Test1」 在專案中,會被AppClassLoader所載入,那麼怎麼使用自己寫的類載入器載入該類呢。

3. 如何讓自定義類載入器載入執行路徑下的指定類

將編譯後的Test1.class檔案移動到另一個指定目錄,例如桌面中(需要在測試類中修改對應的類載入器路徑),然後刪除原來資料夾中編譯的Test1.class檔案,再次執行程式

以下是新的測試類

public static void main(String[] args) throws ClassNotFoundException {

        CustomClassLoader classLoader = new CustomClassLoader("customClassLoader");
        //需要指定類載入器是從什麼路徑下尋找指定類檔案的
        classLoader.setPath("D:/");
        Class<?> loadClass = classLoader.loadClass("jvmtest.Test1");
        System.out.println(loadClass.getClassLoader());
    }

執行測試類,得出以下的結果

findClass..
[customClassLoader]

從結果上看可以得出,Test1類是被我們自定義的類載入器所載入的。分析一下,爲什麼?
首先,這裏我們在構建 CustomClassLoader 類載入器的時候,沒有指定parentClassLoader是什麼類載入器,這裏會呼叫super(name),那麼ClassLoader會預設使用AppClassLoader來作爲預設的類載入器。
所以,這裏會先被AppClassLoader載入,由於雙親委託機制 機製的存在,會逐漸的由擴充套件類,啓動類等類載入器載入,而我們可知,該類最終會被AppClassLoader類嘗試載入,但是由於上面,我們將Test1.class檔案在執行的編譯檔案中刪除掉了,導致執行中AppClassLoader找不到該類,則會讓我們自定義的類載入器來嘗試進行,這時候,檢視原始碼可以得知,會觸發了loadClass()的第三步,findClass(),由於自定義的類載入器重寫了findClass()方法,就進入了我們的findClass()方法,即會列印出我們的結果第一行數據, findClass…
隨後,在指定path目錄下找到了指定檔案,獲取內容後,交由ClassLoader封裝成Class物件,返回。

4. 陣列不同於其他數據型別,載入陣列型別的類載入器是誰呢?

陣列不同於其他數據型別,因爲陣列沒有實際型別,而是在執行時jvm構建的,但是載入陣列型別的類載入器是誰呢?
舉例而說,假如存在一個變數 Object[] objectArr , 載入 objectArr的類載入器其實是由載入Object類的類載入器一致,即啓動類載入器。
這裏還需要區分數據型別是基礎數據型別還是參照型別,如果是基礎數據型別,即原始型別(4類8中種,出了String),jvm規定他們的類載入器爲null,並不代表就是啓動類載入器。
而參照型別按照正常規則來即可。

舉例如下:

package jvmtest;

/**
 * @author ztkj-hzb
 * @Date 2019/12/9 13:56
 * @Description 針對陣列型別而言,陣列型別的類載入器是有其物件型別對應的類載入器來決定的,例如 Xxx[] 的類載入器是有Xxx類的類載入器來決定的。
 *                  針對Integer[] 和 int[]也有區別,雖然結果都是null,但是Integer是有啓動類載入器載入,所以返回null,而int是原生型別,原生型別沒有類載入器,所以
 *                  返回null
 */
public class Test14 {


    public static void main(String[] args) {

        System.out.println(Object[].class);
        System.out.println(Object[].class.getSuperclass());


        int[] intArr = new int[5];
        System.out.println(intArr.getClass().getClassLoader());

        Integer[] integersArr = new Integer[5];
        System.out.println(integersArr.getClass().getClassLoader());

        MyTest14[] myTest14s = new MyTest14[5];
        System.out.println(myTest14s.getClass().getClassLoader());

    }


}

class MyTest14{


}

5. 關於類載入器的父子載入區別

這裏的父子關係,並不是指的是待載入的類的繼承關係,而是指的是類載入器的邏輯父子關係。
例如載入 A類的類載入器是自定義類載入器,載入 B類的類載入器是App類載入器,則可以在A類中呼叫B類,而不能在B類中呼叫A類,因爲B類由AppClassLoader載入,屬於自定義類載入器的邏輯父類別,因爲名稱空間的存在,是無法找到由子類載入器載入的類檔案。

5.1 程式碼原始版本

package jvmtest;

/**
 * @author ztkj-hzb
 * @Date 2019/12/12 17:09
 * @Description
 */
public class MySample {


    public MySample(){
        System.out.println("MySample類載入器:" + this.getClass().getClassLoader());
        //範例化MyCat
        new MyCat();
    }

}

package jvmtest;

/**
 * @author ztkj-hzb
 * @Date 2019/12/12 17:09
 * @Description
 */
public class MyCat {

    public MyCat(){
        System.out.println("MyCat類載入器:" + this.getClass().getClassLoader());
    }

}

package jvmtest;

/**
 * @author ztkj-hzb
 * @Date 2019/12/12 11:47
 * @Description
 */
public class Test16 {


    public static void main(String[] args) throws Exception {


        MyTest15ClassLoader classLoader = new MyTest15ClassLoader("loader1");
        classLoader.setPath("D:/");
        Class<?> loadClass = classLoader.loadClass("jvmtest.MySample");
        System.out.println("classLoader: " + loadClass.getClassLoader());

        //範例化MySample類
        Object instance = loadClass.newInstance();

    }


}

結果如下:

classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
MySample類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2
MyCat類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2

由結果分析可知,因爲建立 MyTest15ClassLoader 類的時候沒有指定父載入器,所以這裏使用預設的父載入器(系統類載入器)。而 MySample類本來就在當前專案編譯的class中存在,所以,這裏使用的是系統類載入器載入,沒有用到自定義的類載入器。

5.2 變動1:在當前專案的編譯的資料夾中刪除MySample.class檔案

在當前專案的編譯的資料夾中刪除MySample.class檔案,再次執行程式碼,得到如下結果:

findClass..
classLoader: [loader1]
MySample類載入器:[loader1]
MyCat類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2

由結果分析可知,由於在當前Runtime環境中,刪除了MySample.class檔案,所以首先通過系統類載入器嘗試載入該類,找不到,則會使用到自定義的類載入器嘗試載入,只要在指定目錄中放入MySample.class檔案,由自定義載入器找到且載入編譯成Class物件,因此可以得到輸出結果的前兩條數據。然後因爲呼叫了 newInstance()範例化方法,觸發了 MySample類的初始化,執行構造方法,於是輸出了第三條數據,在MySample類的構造方法中,觸發了MyCat類的初始化,導致自定義類載入器開始載入MyCat類,同理,首先會嘗試使用系統類載入器來載入MyCat.class,由於當前Runtime環境中,存在MyCat.class檔案,所以,會被系統類載入器所載入該類,因此輸出了最後一條數據。
這裏體現出了,可以由子類載入器載入父類別載入器的類。

5.3 變動2:在當前專案的編譯的資料夾中刪除MyCat.class檔案

在當前專案的編譯的資料夾中刪除MyCat.class檔案,再次執行程式碼,得到如下結果:

classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
MySample類載入器:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: jvmtest/MyCat
    at jvmtest.MySample.<init>(MySample.java:14)
    at java.lang.Class.newInstance(Class.java:442)
    at jvmtest.Test16.main(Test16.java:20)
Caused by: java.lang.ClassNotFoundException: jvmtest.MyCat
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 7 more

由結果分析可知,首先,在Runtime環境中,存在MySample.class檔案,所以必然會被系統類載入器所載入,即出現第一行數據,執行 newInstant方法後,觸發MySample類的初始化,輸出第二行數據,然後在MySample類的構造方法中觸發了MyCat類的初始化,即使用當前的類載入器載入MyCat類,但是在之前,已經將MyCat.class檔案從Runtime環境中刪除了,所以在系統類載入器對應的名稱空間中,找不到MyCat類,就會導致類找不到異常。因此報錯。
這裏體現出了,不能由父類別載入器載入子類載入器名稱空間的類。這就體現出了名稱空間的特性。

5.4 變動3:在當前專案的編譯的資料夾中同時刪除MySample.class和MyCat.class檔案

在當前專案的編譯的資料夾中同時刪除MySimple.class和MyCat.class檔案,再次執行程式碼,得到如下結果:

findClass..
classLoader: [loader1]
MySample類載入器:[loader1]
findClass..
MyCat類載入器:[loader1]

由結果分析可知,首先,在Runtime環境中,不存在MySample.class檔案,所以會被被自定義類載入器所載入,即出現第一行,第二行數據,執行 newInstant方法後,觸發MySample類的初始化,出現第三行,同理,使用自定義類載入器載入MyCat.class,根據雙親委託機制 機製,在系統類載入器名稱空間中無法找到MyCat類,因此還是由自定義類載入器載入,出現第四行和第五行數據。