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執行我們需要的方法了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM