首頁>技術>

類載入器的型別

類載入器有以下種類:

啟動類載入器(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返回擴充套件類載入器。

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

可以看到除了啟動類載入器,其餘的兩個類載入器都繼承於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裡面的流程

loadclass:判斷是否已載入,使用雙親委派模型,請求父載入器,父載入器反饋無法載入,因此使用findclass,讓當前類載入器查詢findclass:當前類載入器根據路徑以及class檔名稱載入位元組碼,從class檔案中讀取位元組陣列,然後使用defineClassdefineclass:根據位元組陣列,返回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方法後,可以看到輸出:

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

破壞雙親委派

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

雙親委派模型是在JDK1.2之後才被引入的,而類載入器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的使用者自定義類載入器的實現程式碼,Java設計者引入雙親委派模型時不得不做出一些妥協。在此之前,使用者去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,jdk為了向前相容,不得已開放對loadClass的重寫操作。

當然,也不止這一次對雙親委派模型的破壞,詳細的文章可以參考破壞雙親委派模型,裡面提到了一個“執行緒上下文類載入器”,對這個不熟悉的同學可以參考真正理解執行緒上下文類載入器(多案例分析)(無法放連結,百度搜索)

12
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Visual Studio Code使用者介面詳解