Java的類載入器與雙親委託機制

某人Valar發表於2020-01-08

作者 某人Valar
如需轉載請保留原文連結

本文涉及到的Java原始碼均為Java8版本

部分圖片來自百度,如有侵權請聯絡刪除

目錄:

  • 類載入器
  • java.lang.ClassLoader類
    • URLClassLoader與SecureClassLoader
    • ClassLoader常見方法原始碼分析
  • 雙親委託機制
    • 圖解
    • 原始碼角度分析
  • 常見的問題分析

前言:我們剛剛接觸Java時,在IDE(整合開發環境) 或者文字編輯器中所寫的都是.java檔案,在編譯後會生成.class檔案,又稱位元組碼檔案。

javac HelloWorld.java   --->  HelloWorld.class
複製程式碼

對於.class檔案來說,需要被載入到虛擬機器中才能使用,這個載入的過程就成為類載入。如果想要知道類載入的方式,就需要知道類載入器雙親委託機制的概念。也就是我們本篇所要介紹的內容。

1. 類載入器

Java中的類載入器可以分為兩種:

  • 系統類載入器
  • 自定義類載入器

而系統類載入器又有3個:

  • Bootstrap ClassLoader:啟動類載入器
  • Extensions ClassLoader:擴充套件類載入器
  • App ClassLoader:也稱為SystemAppClass,系統類載入器

1.1 Bootstrap ClassLoader

Bootstrap ClassLoader用來載入JVM(Java虛擬機器)執行時所需要的系統類,其使用c++實現。

從以下路徑來載入類:

  1. %JAVA_HOME%/jre/lib目錄,如rt.jar、resources.jar、charsets.jar等
  2. 可以在JVM啟動時,指定-Xbootclasspath引數,來改變Bootstrap ClassLoader的載入目錄。

Java虛擬機器的啟動就是通過 Bootstrap ClassLoader建立一個初始類來完成的。 可以通過如下程式碼來得出Bootstrap ClassLoader所載入的目錄:

public class ClassLoaderTest {
    public static void main(String[]args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
    }
}
複製程式碼

列印結果為:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes
複製程式碼

可以發現幾乎都是$JAVA_HOME/jre/lib目錄中的jar包,包括rt.jar、resources.jar和charsets.jar等等。

1.2 Extensions ClassLoader

Extensions ClassLoader(擴充套件類載入器)具體是由ExtClassLoader類實現的,ExtClassLoader類位於sun.misc.Launcher類中,是其的一個靜態內部類。對於Launcher類,可以先看成是Java虛擬機器的一個入口。

ExtClassLoader的部分程式碼如下:

Java的類載入器與雙親委託機制

Extensions ClassLoader負責將JAVA_HOME/jre/lib/ext或者由系統變數-Djava.ext.dir指定位置中的類庫載入到記憶體中。

通過以下程式碼可以得到Extensions ClassLoader載入目錄:

System.out.println(System.getProperty("java.ext.dirs"));
複製程式碼

列印結果為:

C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
複製程式碼

1.3 App ClassLoader

也稱為SystemAppClass(系統類載入器),具體是由AppClassLoader類實現的,AppClassLoader類也位於sun.misc.Launcher類中。

部分程式碼如下:

Java的類載入器與雙親委託機制

  1. 主要載入Classpath目錄下的的所有jar和Class檔案,是程式中的預設類載入器。這裡的Classpath是指我們Java工程的bin目錄。
  2. 也可以載入通過-Djava.class.path選項所指定的目錄下的jar和Class檔案。

通過以下程式碼可以得到App ClassLoader載入目錄:

System.out.println(System.getProperty("java.class.path"));
複製程式碼

列印結果為:

C:\workspace\Demo\bin
複製程式碼

這個路徑其實就是當前Java工程目錄bin,裡面存放的是編譯生成的class檔案。


在Java中,除了上述的3種系統提供的類載入器,還可以自定義一個類載入器。

1.4. 自定義類載入器

為了可以從指定的目錄下載入jar包或者class檔案,我們可以用繼承java.lang.ClassLoader類的方式來實現一個自己的類載入器。

在自定義類載入器時,我們一般複寫findClass方法,並在findClass方法中呼叫defineClass方法。

接下來會先介紹下ClassLoader類相關的具體內容,之後看一個自定義類載入器demo。

2 java.lang.ClassLoader類

2.1 ClassLoader、URLClassLoader與SecureClassLoader的關係

從上面關於ExtClassLoader、AppClassLoader原始碼圖中我們可以看到,他們都繼承自URLClassLoader,那這個URLClassLoader是什麼,其背後又有什麼呢?

先來一張很重要的繼承關係圖:

Java的類載入器與雙親委託機制

  • ClassLoader是一個抽象類,位於java.lang包下,其中定義了ClassLoader的主要功能。
  • SecureClassLoader繼承了抽象類ClassLoader,但SecureClassLoader並不是ClassLoader的實現類,而是擴充了ClassLoader類加入了許可權方面的功能,加強了ClassLoader的安全性。
  • URLClassLoader繼承自SecureClassLoader,用來通過URl路徑從jar檔案和資料夾中載入類和資源。
  • ExtClassLoader和AppClassLoader都繼承自URLClassLoader,它們都是Launcher 的內部類,Launcher 是Java虛擬機器的入口應用,ExtClassLoader和AppClassLoader都是在Launcher中進行初始化的。

2.2 普通的類、AppClassLoader與ExtClassLoader之間的關係

關係:

  • 載入普通的類(這裡指得是我們所編寫的程式碼類,下文demo中的Test類)載入器是AppClassLoader,AppClassLoader的父載入器為ExtClassLoader
  • 而ExtClassLoader的父載入器是Bottstrap ClassLoader

還有2個結論:

  • 每個類都有類載入器
  • 每個類載入器都有父載入器

我們準備一個簡單的demo 自建的一個Test.java檔案。

public class Test{}
複製程式碼
public class Main {
    public static void main(String[] args) {
		ClassLoader cl = Test.class.getClassLoader();
		System.out.println("ClassLoader is:"+cl.toString());
	}
}
複製程式碼

這樣就可以獲取到Test.class檔案的類載入器,然後列印出來。結果是:

sun.misc.Launcher$AppClassLoader@75b83e92
複製程式碼

也就是說明Test.class檔案是由AppClassLoader載入的。

那AppClassLoader是誰載入的呢? 其實AppClassLoader也有一個父載入器,我們可以通過以下程式碼獲取

public class Test {
    public static void main(String[] args) {
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}
複製程式碼

上述程式碼結果如下:

sun.misc.Launcher$AppClassLoader@7565783b
sun.misc.Launcher$ExtClassLoader@1b586d23
複製程式碼
  • 載入Test的類載入器是AppClassLoader,AppClassLoader的父載入器為ExtClassLoader
  • 而ExtClassLoader的父載入器是Bottstrap ClassLoader

至於為何沒有列印出ExtClassLoader的父載入器Bootstrap ClassLoader,這是因為Bootstrap ClassLoader是由C++編寫的,並不是一個Java類,因此我們無法在Java程式碼中獲取它的引用。

2.3 java.lang.ClassLoader類常見的方法

上一節我們看到了ClassLoader的getParent方法,getParent獲取到的其實就是其父載入器。這一節將通過原始碼,來介紹ClassLoader中的一些重要方法。

getParent()
ClassLoader類
---------
public final ClassLoader getParent() {
    if (parent == null) return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(parent, Reflection.getCallerClass());
    }
    return parent;
}
複製程式碼

我們可以看到,其返回值有兩種可能,為或者是parent變數。

從原始碼中還可以發現其是一個final修飾的方法,我們知道被final修飾的說明這個方法提供的功能已經滿足當前要求,是不可以重寫的, 所以其各個子類所呼叫的getParent()方法最終都會由ClassLoader來處理。

parent變數又是什麼呢?我們在檢視原始碼時可以發現parent的賦值是在構造方法中。

ClassLoader類
---------
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ... //省略了無關程式碼
}
複製程式碼

而此構造方法又是私有的,不能被外部呼叫,所以其呼叫者還是在內部。於是接著查詢到了另外兩個構造方法。

ClassLoader類
---------
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
    
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
複製程式碼

所以:

  1. 可以在呼叫ClassLoder的構造方法時,指定一個parent。
  2. 若沒有指定的話,會使用getSystemClassLoader()方法的返回值。

接著看上面程式碼中的getSystemClassLoader的原始碼:

ClassLoader類
---------
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
複製程式碼

其返回的是一個scl。在initSystemClassLoader()方法中發現了對scl變數的賦值。

ClassLoader類
---------
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();
            ...//省略程式碼
        }
        sclSet = true;
    }
}
複製程式碼

重點來了,註釋1處其獲取到的是Launcher類的物件,然後呼叫了Launcher類的getClassLoader()方法。

Launcher類
---------
public ClassLoader getClassLoader() {
    return this.loader;
}
複製程式碼

那這個this.loader是什麼呢?在Launcher類中發現,其賦值操作在Launcher的構造方法中,其值正是Launcher類中的AppClassLoader

Launcher類
---------
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    ...
}
複製程式碼

到這裡謎團全部解開了:

在建立ClassLoder時,

  1. 可以指定一個ClassLoder作為其parent,也就是其父載入器。
  2. 若沒有指定的話,會使用getSystemClassLoader()方法的返回值(也就是Launcher類中的AppClassLoader)作為其parent。
  3. 通過getParent()方法可以獲取到這個父載入器。
defineClass()

能將class二進位制內容轉換成Class物件,如果不符合要求的會丟擲異常,例如ClassFormatErrorNoClassDefFoundError

在自定義ClassLoader時,我們通常會先將特定的檔案讀取成byte[]物件,再使用此方法,將其轉為class物件。

ClassLoader類
---------
/**
* String name:表示預期的二進位制檔名稱,不知道的話,可以填null。
* byte[] b:此class檔案的二進位制資料
* int off:class二進位制資料開始的位置
* int len:class二進位制資料的總長度
*/

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}


protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}
複製程式碼
findClass()

findClass()方法一般被loadClass()方法呼叫去載入指定名稱類。

ClassLoader類
---------
/**
* String name:class檔案的名稱
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
} 
複製程式碼

通過原始碼看到ClassLoader類中並沒有具體的邏輯,而是等待著其子類去實現,通過上面的分析我們知道兩個系統類載入器ExtClassLoaderAppClassLoader都繼承自URLClassLoader,那就來看一下URLClassLoader中的具體程式碼。

URLClassLoader類
---------
protected Class<?> findClass(final String name) throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                        ...
    return result;
}

private Class<?> defineClass(String name, Resource res) throws IOException {
    ...
    URL url = res.getCodeSourceURL();
    ...
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        ...
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        ...
        return defineClass(name, b, 0, b.length, cs);
    }
}
複製程式碼

可以看到其對傳入的name進行處理後,就呼叫了defineClass(name, res);在這個方法裡主要是通過res資源和url,載入出相應格式的檔案,最終還是通過ClassLoader的defineClass方法載入出具體的類。

loadClass()

上節說到findClass()一般是在loadClass()中呼叫,那loadClass()是什麼呢? 其實loadClass()就是雙親委託機制的具體實現,所以在我們先介紹下雙親委託機制後,再來分析loadClass()

3 雙親委託機制介紹

3.1 圖解雙親委託機制

先簡單介紹下雙親委託機制: 類載入器查詢Class(也就是在loadClass時)所採用的是雙親委託模式,所謂雙親委託模式就是

  1. 首先判斷該Class是否已經載入
  2. 如果沒有則不是自身去查詢而是委託給父載入器進行查詢,這樣依次的進行遞迴,直到委託到最頂層的Bootstrap ClassLoader
  3. 如果Bootstrap ClassLoader找到了該Class,就會直接返回
  4. 如果沒找到,則繼續依次向下查詢,如果還沒找到則最後會交由自身去查詢

Java的類載入器與雙親委託機制
(圖片來自http://liuwangshu.cn/application/classloader/1-java-classloader-.html)

  • 其中紅色的箭頭代表向上委託的方向,如果當前的類載入器沒有從快取中找到這個class物件,就會請求父載入器進行操作。直到Bootstrap ClassLoader
  • 而黑色的箭頭代表的是查詢方向,若Bootstrap ClassLoader可以從%JAVA_HOME%/jre/lib目錄或者-Xbootclasspath指定目錄查詢到,就直接返回該物件,否則就讓ExtClassLoader去查詢。
  • ExtClassLoader就會從JAVA_HOME/jre/lib/ext或者-Djava.ext.dir指定位置中查詢,找不到時就交給AppClassLoaderAppClassLoader就從當前工程的bin目錄下查詢
  • 若還是找不到的話,就由我們自定義的CustomClassLoader查詢,具體查詢的結果,就要看我們怎麼實現自定義ClassLoader的findClass方法了。

3.2 原始碼分析雙親委託機制

接下來我們看看雙親委託機制在原始碼中是如何體現的。 先看loadClass的原始碼:

ClassLoader類
---------
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,根據name檢查類是否已經載入,若已載入,會直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //若當前類載入器有父載入器,則呼叫其父載入器的loadClass()
                    c = parent.loadClass(name, false);
                } else {
                    //若當前類載入器的parent為空,則呼叫findBootstrapClassOrNull()
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
        
            if (c == null) {
                // 1.如果到這裡c依然為空的話,表示一直到最頂層的父載入器也沒有找到已載入的c,那就會呼叫findClass進行查詢
                // 2.在findClass的過程中,如果指定目錄下沒有,就會丟擲異常ClassNotFoundException
                // 3.丟擲異常後,此層呼叫結束,接著其子載入器繼續進行findClass操作
                long t1 = System.nanoTime();
                c = findClass(name);

                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

複製程式碼

findBootstrapClassOrNull()方法:可以看到其對name進行校驗後,最終呼叫了一個native方法findBootstrapClass()。在findBootstrapClass()方法中最終會用Bootstrap Classloader來查詢類。

ClassLoader類
---------
private Class<?> findBootstrapClassOrNull(String name)
{
    if (!checkName(name)) return null;
    return findBootstrapClass(name);
}
    
private native Class<?> findBootstrapClass(String name);
複製程式碼

4 常見的問題

4.1 為什麼使用雙親委託機制?

  1. 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
  2. 安全方面的考慮,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這樣便會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類。

4.2 由不同的類載入器載入的類會被JVM當成同一個類嗎?

不會。 在Java中,我們用包名+類名作為一個類的標識。 但在JVM中,一個類用其包名+類名和一個ClassLoader的例項作為唯一標識,不同類載入器載入的類將被置於不同的名稱空間.

通過一個demo來看,

  1. 用兩個自定義類載入器去載入一個自定義的類
  2. 然後獲取到的Class進行java.lang.Object.equals(…)判斷。
public class Main {
    public static void main(String[] args) {
    
        ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");
        ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");
        try {
            Class c = myClassLoader.loadClass("com.example.Hello");
            Class c2 = myClassLoader.loadClass("com.example.Hello");

            Class c3 = myClassLoader2.loadClass("com.example.Hello");

            System.out.println(c.equals(c2)); //true
            System.out.println(c.equals(c3)); //flase
    }
}
複製程式碼

輸出結果:

true
false
複製程式碼

只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類。

上面demo中用到的自定義ClassLoader:

自定義的類載入器
注意點:
1.覆寫findClass方法
2.讓其可以根據name從我們指定的path中載入檔案,也就是將檔案正確轉為byte[]格式
3.使用defineClass方法將byte[]資料轉為Class物件
-------------
public class ClassLoaderTest extends ClassLoader{
    private String path;
    public ClassLoaderTest(String path) {
        this.path = path;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        byte[] classData = classToBytes(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            clazz= defineClass(name, classData, 0, classData.length);
        }
        return clazz;
    }
    private byte[] classToBytes(String name) {
        String fileName = getFileName(name);
        File file = new File(path,fileName);
        InputStream in=null;
        ByteArrayOutputStream out=null;
        try {
            in = new FileInputStream(file);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length=0;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            return out.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try{
                if(out!=null) {
                    out.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        return null;
    }
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}
複製程式碼

結語

到此Java的類載入器以及雙親委託機制都講了個大概,如果文中有錯誤的地方、或者有其他關於類載入器比較重要的內容又沒有介紹到的,歡迎在評論區裡留言,一起交流學習。

下一篇會說道Java new一個物件的過程,其中會涉及到類的載入、驗證,以及物件建立過程中的堆記憶體分配等內容。

參考: liuwangshu.cn/application…

blog.csdn.net/briblue/art…

blog.csdn.net/justloveyou…

相關文章