深入理解JVM(③)虛擬機器的類載入器(雙親委派模型)

紀莫發表於2020-06-28

前言

先解釋一下什麼是類載入器,通過一個類的全限定名來獲取描述該類的二進位制位元組流,在虛擬機器中實現這個動作的程式碼被稱為“類載入器(Class Loader)”。

類與類載入器

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠超類載入階段。每個類載入器都有一個獨立的類名稱空間,所以每個類唯一性都必須是建立在是否為同一個類載入器的前提下的。
否則,即使是兩個類來源於同一個Class檔案,被同一個Java虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等。
例如:

public class ClassLoaderOneTest {

    public static void main(String[] args) throws Exception{

        ClassLoader oneLoader = new ClassLoader() {
            
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {

                try {

                    String classFileName = name.substring(name.lastIndexOf(".")+1)+".class";

                    InputStream inputStream = getClass().getResourceAsStream(classFileName);

                    if(inputStream == null){
                        return super.loadClass(name);
                    }
                    
                    byte[] bytes = new byte[inputStream.available()];

                    inputStream.read(bytes);

                    return defineClass(name,bytes,0,bytes.length);
                }catch (IOException e){
                    throw new ClassNotFoundException(name);

                }
            }
        };

        Object object = oneLoader.loadClass("com.eurekaclient2.test.jvm3.SonClass").newInstance();

        System.out.println(object.getClass());

        System.out.println("instanceof result :"+ (object instanceof com.eurekaclient2.test.jvm3.SonClass));
    }

}

執行結果:

class com.eurekaclient2.test.jvm3.SonClass
instanceof result :false

通過上面的執行結果可以看出,自定義的類載入器載入出來的類建立的物件和com.eurekaclient2.test.jvm3.SonClass在做型別檢查時返回了false,這是因為在Java虛擬機器中存在的兩個SonClass,一個是由虛擬機器的應用程式類載入器所載入的,另一個是由自定義的類載入器載入的,雖然來自同一個Class檔案,但在Java虛擬機器中是兩個互相獨立的類。

雙親委派模型

Java虛擬機器把類載入器分為了兩大類:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器是虛擬機器的一部分。另外一種就是其他類載入器(全部繼承自java.lang.ClassLoader)。
從JDK1.2開始至JDK9之前的Java應用絕大多數都會使用到如下3個系統提供的類載入器進行載入。

  • 啟動類載入器(Bootstrap Class Loader):這個類載入器複製載入存放在<JAVA_HOME>\lib目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,而且是Java虛擬機器能夠識別的類庫載入到虛擬機器的記憶體中。
    如果需要使用引導類載入器去載入類,直接使用null代替即可。
    如下是ClassLoader.getClassLoader()方法的原始碼:
/**
 * Returns the class loader for the class.  Some implementations may use
 * null to represent the bootstrap class loader. This method will return
 * null in such implementations if this class was loaded by the bootstrap
 * class loader.
 * /
@CallerSensitive
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    }
    return cl;
}
  • 擴充套件類載入器(Extension Class Loader):這個類載入器是在類sun.misc.launcher$ExtClassLoader中以Java程式碼的形式實現的。它負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。這是Java系統類庫的擴充套件機制,但是在JDK9之後,被模組化能力所替代了。
  • 應用程式類載入器(Application Class Loader):這個類載入器由sun.misc.Launcher$AppClassLoader來實現的。它負責載入使用者類路徑(ClassPath)上所有的類庫,開發者同樣可以直接在程式碼中使用這個類載入器。如沒有自定義的類載入器,這個就是預設的類載入器。

在JDK9之前的Java應用都是由這三類載入器互相配合來完成載入的,如果有自定義的類載入器,會先執行自定義的類載入器。各種的類載入器之間的層次關係被稱為類載入器的“雙親委派模型(Parents Delegation Model)”。
各種類載入器的關係如下圖:
在這裡插入圖片描述
雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。
這裡的載入器之間的子父關係不是繼承,通常使用組合關係來複用父載入器的程式碼。

雙親委派模型並不是一個強制性約束力的模型而是Java設計者推薦給開發者的一種類載入器實現的最佳實踐。

雙親委派模型的工作過程

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到最頂端的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入才會嘗試自己去完成載入。

使用雙親委派模型來組織類載入器之間的關係的好處就是能保證Java型別體系中最基礎的類的唯一。
例如:類java.lang.Object無論哪一個類載入器載入最終都會委派給啟動類載入器,因此能夠保證各種類載入器環境中的都是同一個類。這樣就能保證我們建立出來的類的擁有最基礎的行為。

記得以前在面試的時候有被問到,讓自己寫一個java.lang.String類然後會被虛擬機器載入執行嗎?這個問題考察的就是類的載入機制的雙親委派模型。

雙親委派模型的原始碼其實非常簡潔,先檢查請求載入的型別是否已經被載入過,若沒有則呼叫父載入器的loadeClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。假如父載入器載入失敗,丟擲ClassNotFoundException異常,才會呼叫自己的findClass方法嘗試進行載入。
原始碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,檢查請求的類是否已經被載入過了
            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
                    // 說明父類載入器無法完成載入請求
                }

                if (c == null) {
                    // 在父類載入器無法載入時
                    // 在呼叫本身的findClass方法來進行類載入
                    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;
        }
    }

破壞雙親委派模型

上面也提到了,雙親委派模型並不是一個具體強制性約束的模型,雖然在Java的世界大部分的類載入器都遵循這個模型,但也有例外的情況,直到JDK9(模組化)為止,主要出現過3次較大規模“被破壞”的情況。

  • 第一次“被破壞”其實發生在雙親委派模型出現之前,由於雙親委派模型在JDK1.2之後才被引入,但是類載入器的概念和抽象類java.lang.ClassLoader則在Java的第一個版本中就已經存在了。為了向前相容,只能在JDK1.2之後的java.lang.ClassLoader中新增一個新的protected方法findClass(),並引導使用者編寫類的載入邏輯時儘可能去重寫這個方法,而不是在loadClass()中編寫程式碼。
  • 第二次“被破壞”是因為自身的一些侷限性導致的,雙親委派模型很好的解決了各個類載入器協作時基礎型別的一致性問題,即越基礎的類由越上層的載入器進行載入。
    但是如果基礎的類需要呼叫下面的使用者程式碼時該怎麼辦呢?Java設計團隊用了一個不太優雅的方案,引入了一個名叫執行緒上下文的類載入器(Thread Context ClassLodar)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。
    像JNDI、JDBC、JCE、JAXB、JBI等都是用的這種型別載入器實現的功能。最常見的tomcat中就用到了執行緒上下文的類載入器。
  • 第三次“破壞”,是為了實現熱部署、模組化。在更新了一部分程式碼後,不需要停機重啟,只需要將類載入器和類都替換掉就可以了。典型的就是OSGi的模組化熱部署。

相關文章