Java 類機制(2)---- 類加載過程

前言

大家好,在該專欄的上一篇文章中我們介紹了一下關於 Java 中類的相關知識點。那麼這篇文章我們來看一下一個 Java 類是怎麼被虛擬機加載並使用的,本文內容參考了《深入理解Java機》一書。

試想一下,如果沒有 Eclipse,IDEA 等 Java 編程工具,我們在編寫好一個 Java 類源文件(.java)後如何將其編譯成一個 .class 文件呢?沒錯,通過 javac 命令,實際上也就是 javac 程序,它一般在你 Java 安裝目錄的 bin 子目錄下:
在這裏插入圖片描述
在這裏我們不僅看到了 javac 命令,還看到了我們非常熟悉的 javajavadocjavahjavap 等程序。其中 javadoc 是爲類生成 html 格式的文檔的程序,javah 是爲某個存在 native 方法的類生成 jni 頭文件的程序,javap 是用來生成某個類的字節碼的程序。好了,讓我們回到最開始接觸 Java 的時候, 來手動編譯並運行一個類吧,我們先新建一個類文件,暫且叫 Main.java

public class Main {
    
    public static void main(String[] args) {
        System.out.println("This is a simple class!");
    }
}

我們來通過命令行進行操作(確保你已經將 java 安裝目錄下的 bin 子目錄成功的添加到環境變量中,即成功配置好 Java 環境):
在這裏插入圖片描述
好了,我們已經成功得到了編譯出來的類文件了。當我們調用 java Main 命令時,會執行這個類中的 main(Stirng[] args) 方法,在這個過程中首先會創建一個虛擬機進程,然後虛擬機會尋找並加載 Main 類,在加載完成後執行其 main 方法。那麼接下來我們來看看虛擬機是如何尋找並加載類的。

類加載過程

要加載一個類,先得找到這個類,因此在上面的命令行中我們先進入了 Main.class 文件所在的目錄,然後調用 java Main 命令,這樣虛擬機就會在當前目錄下尋找名爲 Main 的 class 文件。那麼如果當前命令調用時所在目錄不在 Main.class 文件所在目錄該怎麼辦呢?我們需要使用 -classpath 參數來指定要加載的類所在的目錄:
在這裏插入圖片描述
這裏我在 D盤根目錄下調用 java 命令,通過 -classpath 命令指定 Main 所在的目錄,虛擬機成功加載該類並運行,所以在上面調用 java Main 命令等同於 java -classpath "C:\Users\MAIBENBEN\Desktop\blog\Java常用技術\Java 類機制(2)----類加載過程" Main 命令。那麼如果我有多個類文件目錄(當然本例中沒有)怎麼辦呢?我們只需要在 -classpath 參數值中以分號分隔多個路徑即可:-classpath "C:\Users\MAIBENBEN\Desktop\blog\Java常用技術;C:\Users\MAIBENBEN\Desktop\blog\Java常用技術\Java 類機制(2)----類加載過程"。好了,現在虛擬機已經可以找到我們自定義的類了,可以進行加載這個類的動作了。一般來說,虛擬機要加載一個類,需要經過以下步驟:加載->驗證->準備->解析->初始化。其中,驗證解析準備 這三個過程也被稱爲 鏈接。當這些動作完成並且中途沒有出錯後,類就已經被成功加載到虛擬機中了(生成一個 Class 類型的對象,儲存在方法區 / 堆中,一般是方法區)。之後這個類就可以被使用了。

加載 步驟主要是將要進行加載的類的 .class 文件的二進制流加載進來,但是如何得到類的二進制數據,虛擬機並未進行明確規定,可以是通過命令編譯出來的類文件,也可以是通過網絡傳輸的數據,甚至可以是人爲編輯的二進制文件(如果你對 .class 文件格式足夠熟悉的話)。即加載哪個類數據和如何加載類數據是開發者可自定義的。之後會對加載進來的類數據進行格式解析,如果解析成功(二進制數據符合規定的類數據格式),則會在內存中生對應類的一個 Class 對象,否則拋出 ClassFormatError 異常。

驗證 步驟用來判斷在上一步加載的類是否符合當前虛擬機要求,並且不會危害當前虛擬機安全。從語言特性上來說,Java 類在編譯時就會判斷語法的正確性,那爲什麼虛擬機還要花大功夫來進行驗證呢?正如 加載 部分所述,我們得到的類數據的方法有很多種,可以通過編譯後的 .class 文件,也可以通過網絡等方式。也就是說類數據的來源是未知的,如果這個時候虛擬機不進行驗證的話,很可能加載對虛擬機有害的類數據,其次,即使上一步生成的類對象是一個正常的類,但是由於 Java 語言是在不斷更新的,因此類和虛擬機之間可能會產生版本差,即類本身用到了一些 Java 語言新版本的特性,而當前虛擬機是舊版本的,即其不能處理新版本的語言特性,這種情況也會導致出錯。因此 驗證 這一部分是非常重要的。

準備 階段,虛擬機會將類對象中的 靜態變量 (即被 static 關鍵字修飾的非常量)賦零值,例:

private static int value1 = 3;

對於 shortintlongfloatdouble 數值描述的類型賦值爲 0,boolean 類型賦值爲 falsechar 類型賦值爲 \u0000,引用類型賦值爲 null。具體所指定的值(這裏爲 3)則會在 初始化 階段進行賦值。而對於常量,例:

private static final int value1 = 3;

在該階段會直接賦值爲所指定的值(這裏即爲 3)。

解析 階段虛擬機會針對類和接口、字段、類方法、接口方法、方法類型等進行解析,這個過程會加載該類定義的字段的類型中還未被加載進入虛擬機中的類。

初始化 階段是類加載的最後一個階段,這個階段中虛擬機會調用<cinit> 方法,當然,這個方法不是開發者寫的,而是在編譯器編譯類的過程中編譯器加上的,編譯器會收集靜態變量賦值代碼static{} 代碼塊,將這些代碼放入 <cinit> 方法中,收集的順序即爲代碼塊在類中聲明的順序。上面 準備 步驟中靜態變量的最終值會在該方法中賦值給該靜態變量。同時,虛擬機會保證在子類的 <cinit> 方法執行時,其父類的 <cinit> 已經被執行完畢,其次,每個 <cinit> 方法只會被調用一次。這裏說一下編譯器爲類生成的 <init> 方法和 <cinit> 方法區別:<init> 在創建某個類的實例對象時被調用,而 <cinit> 方法在類加載的初始化階段被調用,對於同一個類對象來說,<init> 方法可能被調用多次(每次實例化該類的一個對象時被調用),而 <cinit> 方法只會被調用一次(類加載時)。這裏給出一個關於類的相關方法調用順序的實踐代碼:

public class InvokeOrder {
    static int v = 1;

    static {
        System.out.println(Thread.currentThread() + ": InvokeOrder cinit invoke: v = " + v);
    }

    int vv = 2;

    {
        System.out.println(Thread.currentThread() + ": InvokeOrder init invoke: vv = " + vv);
    }

    InvokeOrder() {
        System.out.println(Thread.currentThread() + ": InvokeOrder constructor invoke");
    }

    static class SubClass extends InvokeOrder {
        static int x = 3;

        static {
            System.out.println(Thread.currentThread() + ": SubClass: cinit invoke: x = " + x);
        }

        int xx = 4;

        {
            System.out.println(Thread.currentThread() + ": SubClass init invoke: xx = " + xx);
        }

        SubClass() {
            System.out.println(Thread.currentThread() + ": SubClass constructor invoke");
        }
    }

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
    }
}

結果如下所示:
在這裏插入圖片描述
由此,我們可以得出創建一個還未被加載的類的實例對象時相關代碼的執行順序:父類靜態代碼塊->子類靜態代碼塊->父類非靜態代碼塊->父類構造方法->子類非靜態代碼塊->子類構造方法。並且我們還可以發現:虛擬機不會另闢線程去進行類加載,進行類加載的線程即爲執行了導致該類被加載的代碼的線程。

我們在上文中已經知道了在類加載過程中虛擬機允許開發者來自定義要加載的類數據,那麼開發者該通過什麼方式來自定義這個要加載的類數據呢?答案自然是大名鼎鼎的類加載器(ClassLoader)。

我們從上文中已經知道了,整個類加載過程的 準備 階段中獲取類數據的操作是開發者可控的,即具體怎麼加載類數據和加載什麼類數據是可以由開發者決定的。那麼開發者通過什麼控制這個操作呢?自然是大名鼎鼎的類加載器(ClassLoader)。

ClassLoader

我們還是從官方對這個類的說明開始:

/**
 * A class loader is an object that is responsible for loading classes. The
 * class <tt>ClassLoader</tt> is an abstract class.  Given the <a
 * href="#name">binary name</a> 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.
 *
 * <p> Every {@link Class <tt>Class</tt>} object contains a {@link
 * Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
 * it.
 */

大概意思爲類加載負責進行類的加載,同時 ClassLoader 是一個抽象類,其功能是通過給定的類名生成描述這個類的二進制數據。一個典型的場景是將類名轉換爲對應的類文件名並讀取該文件,得到對應的類數據,再生成對應的 Class 對象。每一個 Class 對象都包含一個指向加載它的 ClassLoader 對象的引用字段,可以通過 Class 類的實例方法 getClassLoader() 來的到這個 ClassLoader 對象。

從上面的說明我們可以知道:ClassLoader 具有的能力是解析類的二進制數據來生成對應的 Class 對象,ClassLoader 類提供了 defineClass(byte[] b, int off, int len) 方法來完成這個功能,ClassLoader 類部分源碼如下:

public abstract class ClassLoader {
    // ...
    
    /* 當前 ClassLoader 的父 ClassLoader */
    private final ClassLoader parent;
    
    /**
     * 構造方法,通過 parent 參數指定該類加載器的父類加載器
     */
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    
    /**
     * 默認的構造方法,採用 AppClassLoader 作爲父類加載器,下文會介紹 AppClassLoader
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    
    /**
     * 這個方法爲類加載原理的核心方法,name 參數代表要加載的類名,
     * resolve 參數代表是否需要對加載得到的類進行鏈接操作
     *(鏈接操作即爲我們上面提到的類加載的 驗證、準備和解析過程),
     * 鏈接過程是否執行是可以通過參數來控制的,但是當我們在使用一個類時,這個類一定已經完成了整個類加載的步驟。
     * 如果這個方法成功加載對應的類,則返回對應的 Class 對象,否則拋出對應異常
     */
    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);
            // 如果得到的 Class 對象爲空,證明該類還未被當前類加載器加載過,開始進行委託操作
            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
                }
                // 如果得到的 Class 對象還是爲空,則證明父類加載器和啓動類加載器都不能加載該類,
                // 此時需要調用 findClass(String name) 方法來自己進行加載
                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();
                }
            }
            // 如果需要對得到的 Class 對象進行鏈接,則調用 resolveClass(Class c) 方法進行類鏈接操作
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    /**
     * 查找類名爲 name 的類,返回對應的 Class 對象,這個方法是子類要重寫的方法,
     * 在這個方法中加入自定義加載類的邏輯
     *
     * @since  1.2
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    /**
     * 將參數 b 儲存的數據的 [offset, offset + len - 1] 部分轉換爲一個 Class 對象,
     * 如果數據不符合規定的類數據規範,則拋出 ClassFormatError 異常,這個方法已經被
     * defineClass(String, byte[], int, int) 方法替代
     */
    @Deprecated
    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }
    
    /**
     * 將參數 b 儲存的數據的 [offset, offset + len - 1] 部分轉換爲一個 Class 對象,
     * 如果數據不符合規定的類數據規範,則拋出 ClassFormatError 異常。
     * 最終會經過一系列的驗證(比如 name 能以 java. 開頭),如果驗證不通過,拋出對應異常,
     * 最後調用 native 的方法來進行真正的類定義(創建對應的 Class 對象)
     */
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }
}

當我們要使用某個 ClassLoader 方法加載某個類的時候,我們會調用其 loadClass(String name) 方法,即爲加載名爲 name 的類。我們詳細看看這個方法的代碼註釋,可以總結出類加載的大致流程:先判斷當前 ClassLoader 有沒有加載過該類,如果有,則直接返回,否則判斷 parent 是否爲空,不爲空則調用 parent.loadClass(name) 來委託父 ClassLoader 加載,否則調用 findBootstrapClassOrNull(name); 來委託啓動類加載器加載該類。如果最後得到的 class 還是爲空,則證明該類不能被父 ClassLoader 和啓動類加載器加載。接下來調用自身的 findClass(name); 來加載該類,而默認的 findClass 直接拋出了 ClassNotFoundException 異常,證明需要子類重寫該方法並填充自定義的類加載邏輯。我們之前已經說過了 ClassLoader 已經提供了 defineClass 方法來通過二進制數據(byte[])來得到對應的 Class 對象,所以我們重寫 findClass 方法的一般模板便是:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    // ... 通過某種方式得到類的二進制數據(以 byte[] 類型儲存)
    byte[] classData = getClassData(...);
    return defineClass(classData, ...);
}

所以我們能定義的便是代碼註釋中的 通過某種方式得到類的二進制數據過程,這也是上文介紹類加載步驟中 加載 步驟中提到的 加載哪個類數據和如何加載類數據是開發者可自定義的 過程的具體體現。好了,我們在上文提到了 委託父 ClassLoader 加載委託啓動類加載器加載 這兩個動作,那麼什麼是父加載器和啓動類加載器呢?回答這個問題之前我們先來引出另一個問題:我們知道虛擬機要使用一個類必須先將這個類加載到虛擬機內存中並生成一個 Class 對象。那麼形如 JDK 本身提供的類(ObjectString 等),是怎麼被加載進入虛擬機的呢?

其實就是通過啓動類加載器(BootstrapClassLoader),由於啓動類加載器是通過 C++ 實現的,因此這裏我們無法直接看到對應的源碼。啓動類加載器會負責加載 JDK 安裝目錄下 \jre\lib 等文件夾中的 jar 文件或 -Xbootclasspath 參數指定的文件夾中的類,可通過 sun.boot.class.path 屬性得到對應路徑,而 ObjectString 等類文件就在該文件夾下的 rt.jar 包中:
在這裏插入圖片描述
除了啓動類加載器,JDK 還提供了擴展類加載器(ExtClassLoader),用於加載 JDK 安裝目錄下 \jre\lib\ext 等文件夾中的類,可通過 java.class.path 屬性得到對應路徑。

最後是應用程序類加載器(AppClassLoader),用於加載虛擬機運行時通過 -classpath 參數指定的目錄下的類,可通過 java.class.path 屬性得到對應路徑,上文中我們運行的命令行中通過 -classpath 參數指定的文件夾路徑中的 Main 類就是被應用程序類加載器加載進入虛擬機的。來看一下三個屬性值的值:

public class ClassLoaderTest {

    public static void main(String[] args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

在這裏插入圖片描述
在三者之間,應用程序類加載器(AppClassLoader)的 parent 字段指向擴展類加載器(ExtClassLoader),但是擴展類加載器的 parent 字段爲 null(理論上應該指向啓動類加載器對象,但因爲啓動類加載器通過 C++ 實現,Java 層沒有其對象)。因此在上面委託啓動類加載器加載類時需要調用 findBootstrapClassOrNull(name) 方法來單獨處理。那麼,爲什麼一個 ClassLoader 在加載某個類的時候需要先委託父 ClassLoader 和啓動類加載器加載呢?這其實爲了遵循雙親委派模型。

雙親委派模型

雙親委派模型是 Java 虛擬機默認的類加載機制,也是其推薦的類加載機制,其流程如下:當某個類加載器要加載某個類時,先判斷該類有沒有被當前類加載器加載過,如果加載過,則直接返回對應 Class 對象,否則如果其存在父類加載,則會先委託其父類加載器進行加載這個類,否則委託啓動類加載器加載。對於其父類加載器也是同樣的流程。我們可以用一張圖來表示這個過程:
在這裏插入圖片描述
這也即爲 Bootstrap ClassLoaderExtClassLoader , AppClassLoader 和自定義 ClassLoader 之間的委派關係(自定義 ClassLoaderparentAppClassLoaderAppClassLoaderparentExtClassLoader)。那麼爲什麼要遵循雙親委派模型呢?我們要知道,虛擬機在判斷兩個 Class 是否相等時不僅會判斷這兩個類的類名是否相等, 還會判斷加載這兩個類的 ClassLoader 是否是同一個 ClassLoader,如果我同一個類通過某個 ClassLoader 加載兩次就會生成兩個 Class 對象,那麼這對虛擬機和開發者來說都不是好事,一方面多個同一個類的 Class 對象浪費內存和 CPU(每加載一個 Class 對象需要經過 加載驗證,[鏈接] 過程(鏈接過程可以通過參數控制是否進行)),另一方面對開發者來說也是沒有必要的,因爲對於一個類,一般只要有一個對應的 Class 就足夠了。我們來看一個小例子加深理解:

public class ClassLoaderTest {

    /**
     * 自定義的 ClassLoader,加載指定路徑下的類
     */
    static class CustomClassLoader extends ClassLoader {
        private static final String TO_LOAD_CLASS_NAME = "A";
        // 默認加載的類所在文件夾路徑
        private static final String DEFAULT_CLASS_DIR = "D:/ProjectData/JavaProjects/IDEAProject/Test/out/production/Test/";

        private String classDir;
        
        CustomClassLoader(String classDir) {
            super();
            if (classDir == null) {
                throw new IllegalArgumentException("argument is illegal!");
            }
            this.classDir = classDir;
        }
        
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // 如果要加載的類名爲 A,則不遵循雙親委派模型,強行加載。
            if (name.equals(TO_LOAD_CLASS_NAME)) {
                return findClass(TO_LOAD_CLASS_NAME);
            }
            // 否則遵循雙親委派模型
            return super.loadClass(name);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // 先加載類字節數據,再調用 defineClass 方法生成 Class 對象
            byte[] classData = loadClassData(name);
            if (classData != null) {
                return defineClass(name, classData, 0, classData.length);
            }
            return null;
        }

        /**
         * 讀取類 class 文件的數據,以字節數組返回
         *
         * @param className
         * @return
         */
        private byte[] loadClassData(String className) {
            String classPath = classDir + className.replace('.', '/') + ".class";
            System.out.println(classPath);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(classPath);
                byte[] bs = new byte[1024];
                int readCount;
                while ((readCount = fis.read(bs)) != -1) {
                    bos.write(bs, 0, readCount);
                }
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            } finally {
            	try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            return bos.toByteArray();
        }
    }

    public static void main(String[] args) {
        CustomClassLoader classLoader1 = new CustomClassLoader(CustomClassLoader.DEFAULT_CLASS_DIR);
        CustomClassLoader classLoader2 = new CustomClassLoader(CustomClassLoader.DEFAULT_CLASS_DIR);
        try {
            Class c1 = classLoader1.loadClass(CustomClassLoader.TO_LOAD_CLASS_NAME);
            Class c2 = classLoader2.loadClass(CustomClassLoader.TO_LOAD_CLASS_NAME);
            System.out.println("c1, c2, c1 == c2: " + c1 + ", " + c2.toString() + ", " + (c1 == c2));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

要加載的類 A 定義如下:

public class A {
}

結果如下:
在這裏插入圖片描述
可以看到,在我們強行破壞了雙親委派模型的情況下,即使我們兩次加載同一個類,得到的兩個 Class 對象也是不相等的。同時,一個 ClassLoader 不能對同一個類調用兩次 defineClass 方法,也就是說對於同一個 ClassLoader ,在加載一個類時,不能產生兩個不同的 Class 對象。這也是虛擬機爲了保證類加載時的一致性而設計的。我們加一段代碼來打印一下類 AClassLoader 的父類加載器情況:

static void printClassLoader(Class c) {
    if (c != null) {
        ClassLoader classLoader = c.getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

main 方法中加載完 c1 c2 之後調用這個方法:

printClassLoader(c1);

結果:
在這裏插入圖片描述
可以看到,先是 CustomClassLoader,即爲我們自定義的 ClassLoader,再是 AppClassLoader,再是 ExtClassLoader,由於啓動類加載器(BootstrapClassLoader)爲 C++ 實現,因此擴展類加載器中並沒有其對象,所以並未打印出來。這也驗證了我們上面的結論。

Java 提供的類加載器

Java 本身已經給我們提供了幾個自帶的類加載器:SecureClassLoaderURLClassLoaderExtClassLoader 和 AppClassLoader 。他們之間的繼承關係如下:
在這裏插入圖片描述 在這裏插入圖片描述
注意這裏指的是這些類加載器作爲類的繼承關係,不是雙親委派模型的委派關係。他們的雙親委派關係上面已經說明。我們來簡單看一下這些類加載器。

URLClassLoader

我們來看看官方的代碼相關注釋:

/* This class loader is used to load classes and resources from a search
 * path of URLs referring to both JAR files and directories. Any URL that
 * ends with a '/' is assumed to refer to a directory. Otherwise, the URL
 * is assumed to refer to a JAR file which will be opened as needed.
 * ...
 */
public class URLClassLoader extends SecureClassLoader implements Closeable {
    // ...

    public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        ucp = new URLClassPath(urls);
        this.acc = AccessController.getContext();
    }

    public URLClassLoader(URL[] urls) {
        super();
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        ucp = new URLClassPath(urls);
        this.acc = AccessController.getContext();
    }

大意爲:這個類加載器用來加載由多個 URL 指定的類路徑或者是 JAR 文件 ,如果某個 URL 路徑以 / 結尾,則該 URL 代表的是路徑,否則代表的是某個 JAR 文件。同時我們可以看到 URLClassLoader 提供的兩個公有構造方法接收 URL 數組作爲參數,也就是可以從多個 URL 中加載對應的類。

既然 Java 已經提供給我們加載某個特定路徑下的類的類加載器。那麼我們來實踐一次:使用 URLClassLoader 來完成我們上面自定義的類加載器:

public class CustomURLClassLoader extends URLClassLoader {

        public CustomURLClassLoader(URL[] urls) {
            super(urls);
        }
    	
    	public static void main(String[] args) {
        String classPath = "file://D:/ProjectData/JavaProjects/IDEAProject/Test/out/production/Test/";
        try {
            CustomURLClassLoader customURLClassLoader = new CustomURLClassLoader(new URL[]{new URL(classPath)});
            Class c = customURLClassLoader.loadClass("A");
            System.out.println("loaded class: " + c);
        } catch (MalformedURLException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

看看結果:
在這裏插入圖片描述
成功!我們通過系統提供的類加載器通過非常簡短的代碼就完成了我們上面自定義的類加載器,既然 URLClassLoader 接收 URL 對象作爲參數,那麼其可不可以加載網絡上的 class 文件呢?答案是肯定的!我們把上面 main 方法代碼中的 classPath 換成一個類文件下載鏈接:

String classPath = "https://raw.githubusercontent.com/StarkZhidian/JavaComponentProject/master/";

在這裏我將 A.class 文件上傳到了 Github 上,因此這裏直接使用 Github 提供的文件下載鏈接,再看看結果:
在這裏插入圖片描述
依然可以!證明 URLClassLoader 不僅可以加載本地的類,還可以加載網絡上的類,其實這也正好符合 URL 的含義(統一資源定位符,這個資源不一定是在本地)。

數組類的加載和被動引用

我們上面已經討論過了一般類的加載,那麼數組類的加載是怎麼樣的呢?這個需要對數組進行分類討論:如果數組類爲基本類型數組(byte[], char[], short[], int[], float[], long[], double[]),則由虛擬機進行加載,如果這個數組類爲對象數組類,則依然由數組對象類型對應的類加載器進行加載,來看一個實踐代碼:

ClassLoader loaders = int[].class.getClassLoader();
System.out.println(loaders);
loaders = int[][].class.getClassLoader();
System.out.println(loaders);
// 使用了上面自定義的 CustomURLClassLoader 類
loaders = CustomURLClassLoader[].class.getClassLoader();
System.out.println(loaders);

結果:
在這裏插入圖片描述
那麼是不是代碼中引用了某個類這個類就一定會被加載呢?其實並不是,當這個類是被動引用的時候,不會觸發加載。那麼何爲被動引用呢?分幾種情況:

引用到某個類的常量,不會觸發這個類的加載(因爲在編譯期間將常量儲存到調用類的常量池中了)。

定義某個類的數組,不會觸發這個類的加載。

通過子類引用父類的靜態變量,不會觸發子類的加載。我們來實踐一下:

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 引用某個類定義的常量
        System.out.println(ParentClass.W);
        // 定義某個類的數組
        ParentClass[] parentClasses = new ParentClass[1];
    }

	static class ParentClass {
        static final int W = 2;
        static int x = 1;
         static {
            System.out.println("ParentClass: I'm be loading!");
        }
    }

    static class ChildrenClass extends ParentClass {
        static {
            System.out.println("ChildrenClass: I'm be loading!");
        }
    }
}

結果:
在這裏插入圖片描述
可以看到,前兩種情況並未導致 ParentClass 類的加載,那麼我們在 main 方法後面加一句代碼:

public static void main(String[] args) {
    // 引用某個類定義的常量
    System.out.println(ParentClass.W);
    // 定義某個類的數組
    ParentClass[] parentClasses = new ParentClass[1];
    // 通過子類訪問父類的靜態變量
    System.out.println(ChildrenClass.x);
}

結果:
在這裏插入圖片描述
第三步通過子類訪問父類的靜態變量,導致了父類的加載,但是並未導致子類的加載(沒有執行子類的 static 代碼塊),這也證明了我們上面的結論。

好了,這篇文章中我們詳細看了一下關於 JVM中類加載的機制,下一篇文章我們將一起研究一下 class 文件的格式,屆時會再度回顧這篇文章的某些內容。如果覺得本篇文章對您有幫助,不妨動動手指點個贊鼓勵一下~

如果博客中有什麼不正確的地方,請多多指點。

謝謝觀看。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章