深入理解JVM(2)—類(lèi)加載器與類(lèi)加載機(jī)制

類(lèi)加載器(ClassLoader)是負(fù)責(zé)讀取 Java 字節(jié)碼,并轉(zhuǎn)換成 java.lang.Class 類(lèi)的一個(gè)實(shí)例的代碼模塊。
類(lèi)加載器除了用于加載類(lèi)外,還可用于確定類(lèi)在Java虛擬機(jī)中的唯一性。在整個(gè)JVM里,縱然全限定名相同,若類(lèi)加載器不同,則仍然不算作是同一個(gè)類(lèi),無(wú)法通過(guò) instanceOf 、equals 等方式的校驗(yàn)。

1. 類(lèi)加載器

類(lèi)加載器層級(jí)關(guān)系如下圖所示, Bootstrap、Extension、Application 三者并非是繼承關(guān)系,而是子類(lèi)加載器指派父類(lèi)加載器為自己的 parent屬性。由于啟動(dòng)類(lèi)加載器是內(nèi)嵌于 JVM 且無(wú)法被引用,因此 Extension Classloader 設(shè)置null為parent,即等同于指派啟動(dòng)類(lèi)加載器為自己的父加載器。

類(lèi)加載器

1.1 Bootstrap ClassLoader
由C++語(yǔ)言實(shí)現(xiàn)的,并不是繼承自java.lang.ClassLoader,沒(méi)有父類(lèi)加載器。
加載Java核心類(lèi)庫(kù),如:$JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路徑下的包,用于提供JVM運(yùn)行所需的包。
它加載擴(kuò)展類(lèi)加載器和應(yīng)用程序類(lèi)加載器,并成為他們的父類(lèi)加載器。

1.2 Extension ClassLoader
Java語(yǔ)言編寫(xiě),繼承自java.lang.ClassLoader,父類(lèi)加載器為啟動(dòng)類(lèi)加載器。
負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包。從系統(tǒng)屬性:java.ext.dirs目錄中加載類(lèi)庫(kù),或者從JDK安裝目。錄:jre/lib/ext目錄下加載類(lèi)庫(kù)。我們可以將我們自己的包放在以上目錄下,就會(huì)自動(dòng)加載進(jìn)來(lái)了。

1.3 Application ClassLoader
Java語(yǔ)言編寫(xiě),繼承自java.lang.ClassLoader,父類(lèi)加載器為啟動(dòng)類(lèi)加載器
它負(fù)責(zé)加載環(huán)境變量classpath或者系統(tǒng)屬性java.class.path指定路徑下的類(lèi)庫(kù)
是程序中默認(rèn)的類(lèi)加載器,我們Java程序中的類(lèi),都是由它加載完成的。
我們可以通過(guò)ClassLoader#getSystemClassLoader()獲取并操作這個(gè)加載器。

1.4 User ClassLoader
Java語(yǔ)言編寫(xiě),根據(jù)自身需要實(shí)現(xiàn)ClassLoader自定義加載class,如tomcat、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader。通過(guò)靈活定義classloader的加載機(jī)制,我們可以完成很多事情,例如解決類(lèi)沖突問(wèn)題,實(shí)現(xiàn)熱加載以及熱部署,甚至可以實(shí)現(xiàn)jar包的加密保護(hù)。實(shí)現(xiàn)自定義ClassLoader的示例參考章節(jié)3自定義類(lèi)加載器

  // App ClassLoader
  System.out.println(this.getClass().getClassLoader());
  // Ext ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent());
  // Bootstrap ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent().getParent());
  // Bootstrap ClassLoader
  System.out.println(new String().getClass().getClassLoader());

輸出結(jié)果如下,Bootstrap ClassLoader屬于JVM的范疇,所以是null

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ac42916
null
null

3. 加載類(lèi)的方式

1. 隱式加載
通過(guò)new隱式加載,創(chuàng)建對(duì)象時(shí),如果類(lèi)未加載也會(huì)嘗試加載,例如 Student s = new Student();會(huì)嘗試加載Student類(lèi)
2. 顯示加載:

  1. 通過(guò)Class.forName(類(lèi)全限定名)加載
    public static Class<?> forName(String className) throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

其中forName0() 方法調(diào)用中的參數(shù) true 表示要初始化該類(lèi)。包括:

  • 執(zhí)行靜態(tài)代碼塊
  • 初始化靜態(tài)域

例如com.mysql.jdbc.Driver就通過(guò)靜態(tài)代碼塊想DriverManager中注冊(cè)自己。當(dāng)然forName(String name, boolean initialize,ClassLoader loader)也支持指定初始化(initialize)和類(lèi)加載器(loader)

  1. 通過(guò)ClassLoaderloadClass()方法加載類(lèi)

4. 類(lèi)加載機(jī)制

4.1 全盤(pán)負(fù)責(zé)
當(dāng)一個(gè)類(lèi)加載器負(fù)責(zé)加載某個(gè)類(lèi)時(shí),該類(lèi)所依賴(lài)的和引用的其他類(lèi)也將由該類(lèi)加載器負(fù)責(zé)加載,除非顯示使用另外一個(gè)類(lèi)加載器來(lái)加載。
4.2 雙親委派
雙親委派(Parent Delegation),是一個(gè)非常糟糕的翻譯,但是因?yàn)槭褂幂^廣所以一直沿用至今, 雙親委派也叫作“父類(lèi)委托”,是指子類(lèi)加載器如果沒(méi)有加載過(guò)該目標(biāo)類(lèi),就先委托父類(lèi)加載器加載該目標(biāo)類(lèi),只有在父類(lèi)加載器找不到字節(jié)碼文件的情況下才從自己的類(lèi)路徑中查找并加載目標(biāo)類(lèi)。
雙親委派的機(jī)制如ClassLoader中l(wèi)oadClass方法所示:

  1. findLoadedClass,內(nèi)部調(diào)用native方法,在虛擬機(jī)內(nèi)存中查找是否已經(jīng)加載過(guò)此類(lèi)
  2. 如果沒(méi)有加載,則委派父類(lèi)加載,對(duì)于Extension ClassLoader,parent是null,所以通過(guò)findBootstrapClassOrNull委派Bootstrap ClassLoader加載。
  3. 如果父類(lèi)沒(méi)有加載,則通過(guò)findClass(name)自己嘗試加載。
    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;
        }

5. 自定義類(lèi)加載器

“雙親委派”機(jī)制只是Java推薦的,并不是強(qiáng)制的機(jī)制。我們可以繼承java.lang.ClassLoader類(lèi),實(shí)現(xiàn)自己的類(lèi)加載器。如果想保持雙親委派模型,只需要重寫(xiě)findClass(name)方法;如果想破壞雙親委派模型,則重寫(xiě)loadClass(name)方法。
如下MyClassLoader,通過(guò)重寫(xiě)findClass,從MyClassLoader.setRoot 指定的目錄加載編譯后的.class 文件。注意文件路徑不能是classpath路徑, 防止要加載的類(lèi)被Application ClassLoader加載。

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // fileName處理邏輯需要根據(jù)實(shí)際情況修改,保證能夠找到文件
        String[] name = className.split("\\.");
        String fileName = root + File.separatorChar + name[name.length - 1] + ".class";
        try {
            return Files.readAllBytes(Paths.get(fileName));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("指定目錄");
        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("類(lèi)的全限定名");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
            System.out.println(object.getClass().getClassLoader().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. SPI機(jī)制是否打破雙親委派

關(guān)于SPI(Service Provider Interface)是否打破雙親委派,眾說(shuō)紛紜,這里先拋出結(jié)論:沒(méi)有打破雙親委派。
雙親委派機(jī)制是指,子類(lèi)加載器加載類(lèi)之前,先去父類(lèi)加載器中查找,一直查到最基礎(chǔ)的啟動(dòng)類(lèi)加載器,如果都沒(méi)有加載,則嘗試自行加載。根據(jù)雙親委派原理,父類(lèi)加載器加載的類(lèi)對(duì)子類(lèi)可見(jiàn),反之則不成立。
這里以JDBC(Java Database Connectivity,Java數(shù)據(jù)庫(kù)連接池)為例進(jìn)行說(shuō)明。使用JDBC的示例代碼如下:

String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "1234");

1. DriverManager加載
根據(jù)包名可知java.sql.DriverManager是由啟動(dòng)類(lèi)加載器加載,在加載時(shí),通過(guò)靜態(tài)代碼塊調(diào)用loadInitialDrivers()方法, loadInitialDrivers()通過(guò)SPI的方式加載java.sql.Driver的實(shí)現(xiàn),這里是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver,加載過(guò)程如下,省略部分非核心代碼:

    private static void loadInitialDrivers() {
        String drivers;

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    }

2. java.mysql.jdbc.Driver 加載
SPI的機(jī)制這里不展開(kāi)講,核心原理是通過(guò)mysql-connector-java.jar中的META-INF/services/java.sql.Driver文件,找到java.sql.Driver的具體實(shí)現(xiàn),然后加載實(shí)現(xiàn)。
loadInitialDrivers()中的load() 實(shí)現(xiàn)如下所示,注意通過(guò)Thread.currentThread().getContextClassLoader()獲得應(yīng)用類(lèi)加載器,賦值給loader成員變量。

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

SPI中,最終通過(guò)Thread.currentThread().getContextClassLoader()獲得的應(yīng)用類(lèi)加載器加載com.mysql.jdbc.Driver,代碼如下:

        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,  "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        }

cn這里就是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver。調(diào)用c.newInstances時(shí), 會(huì)執(zhí)行com.mysql.jdbc.Driver中的靜態(tài)代碼塊,即向DriverManager注冊(cè)自己。

以上穿插的源碼比較多,又夾雜著SPI的源碼,所以比較混亂,這里做一下總結(jié):

  1. java.sql.DriverManager 是由啟動(dòng)類(lèi)加載器加載,在加載時(shí),通過(guò)SPI加載java.sql.Driver的實(shí)現(xiàn),即com.mysql.jdbc.Driver。
  2. com.mysql.jdbc.Driver是由應(yīng)用類(lèi)加載器負(fù)責(zé)加載的
  3. 父類(lèi)加載器(Bootstrap ClassLoader)通過(guò)線程上下文類(lèi)加載器(ContextClassLoader)去請(qǐng)求子類(lèi)加載器(Application ClassLoader)完成類(lèi)加載的行為,看似打破了雙親委派模型來(lái)逆向使用類(lèi)加載器,晚上所有的打破雙親委派也是指這一過(guò)程。但是子類(lèi)加載器的加載也是走雙親委派流程,先委托給父類(lèi)加載器,加載不到再?lài)L試自行加載,因此完全沒(méi)有破壞雙親委派。


    image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容