天天看點

【JVM】類加載器與雙親委派模型

類加載器的類型

類加載器有以下種類:

  • 啟動類加載器(Bootstrap ClassLoader)
  • 擴充類加載器(Extension ClassLoader)
  • 應用類加載器(Application ClassLoader)

啟動類加載器

内嵌在JVM核心中的加載器,由C++語言編寫(是以也不會繼承ClassLoader),是類加載器層次中最頂層的加載器。用于加載java的核心類庫,即加載jre/lib/rt.jar裡所有的class。由于啟動類加載器涉及到虛拟機本地實作細節,我們無法擷取啟動類加載器的引用。

擴充類加載器

它負責加載JRE的擴充目錄,jre/lib/ext或者由java.ext.dirs系統屬性指定的目錄中jar包的類。父類加載器為啟動類加載器,但使用擴充類加載器調用getParent依然為null。

應用類加載器

又稱系統類加載器,可用通過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類加載器的執行個體,系統類加載器也是以得名。應用類加載器主要加載classpath下的class,即使用者自己編寫的應用編譯得來的class,調用getParent傳回擴充類加載器。

擴充類加載器與應用類加載器繼承結構如圖所示:

【JVM】類加載器與雙親委派模型

可以看到除了啟動類加載器,其餘的兩個類加載器都繼承于ClassLoader,我們自定義的類加載,也需要繼承ClassLoader。

雙親委派機制

當一個類加載器收到了一個類加載請求是,它自己不會先去嘗試加載這個類,而是把這個請求轉交給父類加載器,每一個層的類加載器都是如此,是以所有的類加載請求都應該傳遞到最頂層的啟動類加載器中。隻有當父類加載器在自己的加載範圍内沒有搜尋到該類時,并向子類回報自己無法加載後,子類加載器才會嘗試自己去加載。

ClassLoader内的loadClass方法,就很好的解釋了雙親委派的加載模式:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //檢查該class是否已經被目前類加載器加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
              //此時該class還沒有被加載
                try {
                    if (parent != null) {
                      //如果父加載器不為null,則委托給父類加載
                        c = parent.loadClass(name, false);
                    } else {
                       //如果父加載器為null,說明目前類加載器已經是啟動類加載器,直接時候用啟動類加載器去加載該class
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //此時父類加載器都無法加載該class,則使用目前類加載器進行加載
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    ...
                }
            }
            //是否需要連接配接該類
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }      

為什麼要使用雙親委派機制,就使用目前的類加載器去加載不就行了嗎?為啥搞得這麼複雜呢?

假設現在并沒有雙親委派機制,有這樣的一個場景:

使用者寫了一個Student類,點選運作,此時編譯完成後,虛拟機開始加載class,該class會由應用加載器進行加載,由于Object類是Student的父類,且雙親委派機制不存在的情況下,應用加載器就會自己嘗試加載Object類,但是使用者壓根沒定義Object,即應用加載器無法在加載範圍搜尋到該類,是以此時Object類無法被加載,使用者寫的代碼無法運作。

假設該使用者自己定義了一個Object類,此時再次運作後,應用類加載器則會正常加載使用者定義的Object與Student類。Student類中會調用System.out.print()輸出Student對象,此時會由啟動類加載器加載System類,在此之前同樣也會加載Object類。

此時,方法區中有了兩份Object的中繼資料,Object類被重複加載了!

倘若使用者定義的Object類不安全,可能直接造成虛拟機崩潰或者引起重大安全問題。

如果現在使用雙親委派機制,使用者雖然自己定義了Object類,可以通過編譯,但是永遠不會被記載進方法區。

雙親委派機制避免了重複加載,也保證了虛拟機的安全。

自定義類加載器

我們整理ClassLoader裡面的流程

  1. loadclass:判斷是否已加載,使用雙親委派模型,請求父加載器,父加載器回報無法加載,是以使用findclass,讓目前類加載器查找
  2. findclass:目前類加載器根據路徑以及class檔案名稱加載位元組碼,從class檔案中讀取位元組數組,然後使用defineClass
  3. defineclass:根據位元組數組,傳回Class對象

我們在ClassLoader裡面找到findClass方法,發現該方法直接抛出異常,應該是留給子類實作的。

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

到這裡,我們應該明白,loadClass方法使用了模版方法模式,主線邏輯是雙親委派,但如何将class檔案轉化為Class對象的步驟,已經交由子類去實作。對模版方法模式不熟悉的同學,可以先參考我的另外一篇文章​​模版方法模式​​

其實源碼中,已經有一個自定義類加載的樣例代碼,在注視中:

class NetworkClassLoader extends ClassLoader {
          String host;
          int port;
 
          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }
 
          private byte[] loadClassData(String name) {
              // load the class data from the connection
             
          }
      }      

看得出來,如果我們需要自定義類加載器,隻需要繼承ClassLoader,并且重寫findClass方法即可。

現在有一個簡單的樣例,class檔案依然在檔案目錄中:

package com.yang.testClassLoader;

import sun.misc.Launcher;

import java.io.*;

public class MyClassLoader extends ClassLoader {

    /**
     * 類加載路徑,不包含檔案名
     */
    private String path;


    public MyClassLoader(String path) {
        super();
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = getBytesFromClass(name);
        assert bytes != null;
        //讀取位元組數組,轉化為Class對象
        return defineClass(name, bytes, 0, bytes.length);
    }

    //讀取class檔案,轉化為位元組數組
    private byte[] getBytesFromClass(String name) {
        String absolutePath = path + "/" + name + ".class";
        FileInputStream fis = null;
        ByteArrayOutputStream bos = null;
        try {
            fis = new FileInputStream(new File(absolutePath));
            bos = new ByteArrayOutputStream();
            byte[] temp = new byte[1024];
            int len;
            while ((len = fis.read(temp)) != -1) {
                bos.write(temp, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != fis) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != bos) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader classLoader = new MyClassLoader("C://develop");
        Class test = classLoader.loadClass("Student");
        test.newInstance();
    }
}      

Student類:

public class Student {
    public Student() {
        System.out.println("student classloader is" + this.getClass().getClassLoader().toString());
    }
}      

注意,這個Student類千萬不要加包名,idea報錯不管他即可,然後使用javac Student.java編譯該類,将生成的class檔案複制到c://develop下即可。

運作MyClassLoader的main方法後,可以看到輸出:

【JVM】類加載器與雙親委派模型

看得出來,Student.class确實是被我們自定義的類加載器給加載了。

破壞雙親委派

從上面的自定義類加載器的内容中,我們應該可以猜到了,破壞雙親委派直接重寫loadClass方法就完事了。事實上,我們确實可以重寫loadClass方法,畢竟這個方法沒有被final修飾。雙親委派既然有好處,為什麼jdk對loadClass開放重寫呢?這要從雙親委派引入的時間來看: