1. 程式人生 > 其它 >Java-類載入器(Classloader)

Java-類載入器(Classloader)

概念

Java類載入器(Java Classloader)是Java執行時環境(Java Runtime Environment)的一部分,負責動態載入Java類到Java虛擬機器的記憶體空間中,用於載入系統、網路或者其他來源的類檔案。Java原始碼通過javac編譯器編譯成類檔案,然後JVM來執行類檔案中的位元組碼來執行程式。

類檔案編譯流程圖

我們以下圖為例子,比如我們建立一個ClassLoaderTest.java檔案執行,經過javac編譯,然後生成ClassLoaderTest.class檔案。這個java檔案和生成的class檔案都是儲存在我們的磁碟當中。但如果我們需要將磁碟中的class檔案在java虛擬機器記憶體中執行,需要經過一系列的類的生命週期(載入、連線(驗證-->準備-->解析)和初始化操作,最後就是我們的java虛擬機器記憶體使用自身方法區中位元組碼二進位制資料去引用堆區的Class物件。

通過這個流程圖,我們就很清楚地瞭解到類的載入就是由java類載入器實現的,作用將類檔案進行動態載入到java虛擬機器記憶體中執行。

建立TestHelloWorld.java

編譯java檔案為class檔案 -> javac TestHelloWorld.java

生成編譯好的TestHelloWorld.class檔案

使用java 檢視編譯好的class檔案內容

javap -c -p -l TestHelloWorld.class

JVM在執行TestHelloWorld之前會先解析class二進位制內容,JVM執行的其實就是如上javap命令生成的位元組碼。

類載入器分類

引導類載入器(BootstrapClassLoader)

引導類載入器(BootstrapClassLoader),底層原生程式碼是C++語言編寫,屬於jvm一部分,不繼承java.lang.ClassLoader類,也沒有父載入器,主要負責載入核心java庫(即JVM本身),儲存在/jre/lib/rt.jar目錄當中。(同時處於安全考慮,BootstrapClassLoader只加載包名為java、javax、sun等開頭的類)。

擴充套件類載入器(ExtensionsClassLoader)

擴充套件類載入器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader類實現,用來在/jre/lib/ext或者java.ext.dirs中指明的目錄載入java的擴充套件庫。Java虛擬機器會提供一個擴充套件庫目錄,此載入器在目錄裡面查詢並載入java類。

App類載入器/系統類載入器(AppClassLoader)

App類載入器/系統類載入器(AppClassLoader),由sun.misc.Launcher$AppClassLoader實現,一般通過通過(java.class.path或者Classpath環境變數)來載入Java類,也就是我們常說的classpath路徑。通常我們是使用這個載入類來載入Java應用類,可以使用ClassLoader.getSystemClassLoader()來獲取它。

自定義類載入器(UserDefineClassLoader)

自定義類載入器(UserDefineClassLoader),除了上述java自帶提供的類載入器,我們還可以通過繼承java.lang.ClassLoader類的方式實現自己的類載入器。

雙親委派機制

雙親委派機制的概念

通常情況下,我們就可以使用JVM預設三種類載入器進行相互配合使用,且是按需載入方式,就是我們需要使用該類的時候,才會將生成的class檔案載入到記憶體當中生成class物件進行使用,且載入過程使用的是雙親委派模式,及把需要載入的類交由父載入器進行處理。

如上圖類載入器層次關係,我們可以將其稱為類載入器的雙親委派模型。但注意的是,他們之間並不是"繼承"體系,而是委派體系。當上述特定的類載入器接到載入類的請求時,首先會先將任務委託給父類載入器,接著請求父類載入這個類,當父類載入器無法載入時(其目錄搜素範圍沒有找到所需要的類時),子類載入器才會進行載入使用。這樣可以避免有些類被重複載入。

雙親委派機制的好處

1、這樣就是能夠實現有些類避免重複載入使用,直接先給父載入器載入,不用子載入器再次重複載入。

2、保證java核心庫的型別安全。比如網路上傳輸了一個java.lang.Object類,通過雙親模式傳遞到啟動類當中,然後發現其Object類早已被載入過,所以就不會載入這個網路傳輸過來的java.lang.Object類,保證我們的java核心API庫不被篡改,出現類似使用者自定義java.lang.Object類的情況。

核心方法

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 {
                    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;
        }

在loadClass方法中,它先使用了findLoadedClass(String)方法來檢查這個類是否被載入過。

接著使用父載入器呼叫loadClass(String)方法,如果父載入器為null,類載入器載入jvm內建的載入器。

之後就呼叫findClass(String) 方法裝載類。

最後通過上述步驟我們找到了對應的類,並且接收到的resolve引數的值為true,那麼就會呼叫resolveClass(Class)方法來處理類。

findCLass:查詢指定的Java類

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

findLoadedClass:查詢JVM已經載入過的類

protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

defineClass:定義一個Java類,將位元組碼解析成虛擬機器識別的Class物件。往往和findClass()方法配合使用

protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }

resolveClass:連結指定Java類

protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }

    private native void resolveClass0(Class c);

ClassLoader類載入流程

理解Java類載入機制並非易事,這裡我們以一個Java的HelloWorld來學習ClassLoader

ClassLoader載入com.tyut.TestHelloWorld類重要流程如下:

  1. ClassLoader會呼叫public Class<?> loadClass(String name)方法載入com.tyut.TestHelloWorld類。
  2. 呼叫findLoadedClass方法檢查TestHelloWorld類是否已經初始化,如果JVM已初始化過該類則直接返回類物件。
  3. 如果建立當前ClassLoader時傳入了父類載入器(new ClassLoader(父類載入器))就使用父類載入器載入TestHelloWorld類,否則使用JVM的Bootstrap ClassLoader載入。
  4. 如果上一步無法載入TestHelloWorld類,那麼呼叫自身的findClass方法嘗試載入TestHelloWorld類。
  5. 如果當前的ClassLoader沒有重寫了findClass方法,那麼直接返回類載入失敗異常。如果當前類重寫了findClass方法並通過傳入的com.tyut.TestHelloWorld類名找到了對應的類位元組碼,那麼應該呼叫defineClass方法去JVM中註冊該類。
  6. 如果呼叫loadClass的時候傳入的resolve引數為true,那麼還需要呼叫resolveClass方法連結類,預設為false。
  7. 返回一個被JVM載入後的java.lang.Class類物件。

自定義類載入過程

定義被載入的類 TestHelloWorld

需要被載入的類 TestHelloWorld.java

//TestHelloWorld.java

package com.tyut;

public class TestHelloWorld {
    public String hello() {
        return "Hello World~";
    }
}

自定義載入器 TestClassLoader.java

使用自定義類載入器重寫findClass方法,然後在呼叫defineClass方法的時候傳入TestHelloWorld類的位元組碼的方式來向JVM中定義一個TestHelloWorld類,最後通過反射機制就可以呼叫TestHelloWorld類的hello方法了

//TestClassLoader.java

package com.tyut;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class TestClassLoader extends ClassLoader {
    // TestHelloWorld類名
    private static String testClassName = "com.tyut.TestHelloWorld";

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
//         只處理TestHelloWorld類
        if (name.equals(testClassName)) {
            byte[] classData = getClassData("com.tyut.TestHelloWorld");
            // 呼叫JVM的native方法向JVM定義TestHelloWorld類
            return defineClass(testClassName, classData, 0, classData.length);
        }

        return super.findClass(name);


    }

    public static byte[] getClassData(String testClassName) {
        InputStream is = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = new FileInputStream(testClassName);
            int temp = -1;
            while ((temp = is.read()) != -1) {
                baos.write(temp);


            }

            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}


使用載入器呼叫類

建立一個主函式,使用自定義載入器呼叫我們的類

package com.tyut;


import java.lang.reflect.Method;

public class testmain {
    public static void main(String[] args) throws Exception {
        try {
            TestClassLoader clsload  = new TestClassLoader();
            // 使用自定義的類載入器載入TestHelloWorld類
            Class testClass = clsload.loadClass("com.tyut.TestHelloWorld");

            // 反射建立TestHelloWorld類,等價於 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射獲取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射呼叫hello方法,等價於 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    }


個人理解:

類載入器就是去載入一個我們需要的類,在雙親委派機制都無法載入的情況下,進行本地載入,重新定義 findClass,在方法裡面定義我們所需要的內容,最後使用defineClass向JVM定義,我們就可以使JVM執行我們需要的方法了。