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

做者 某人Valar
如需轉載請保留原文連接html

本文涉及到的Java源碼均爲Java8版本java

部分圖片來自百度,若有侵權請聯繫刪除c++

目錄:緩存

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

前言:咱們剛剛接觸Java時,在IDE(集成開發環境) 或者文本編輯器中所寫的都是.java文件,在編譯後會生成.class文件,又稱字節碼文件。安全

javac HelloWorld.java   --->  HelloWorld.class
複製代碼

對於.class文件來講,須要被加載到虛擬機中才能使用,這個加載的過程就成爲類加載。若是想要知道類加載的方式,就須要知道類加載器雙親委託機制的概念。也就是咱們本篇所要介紹的內容。bash

1. 類加載器

Java中的類加載器能夠分爲兩種:app

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

而系統類加載器又有3個:編輯器

  • Bootstrap ClassLoader:啓動類加載器
  • Extensions ClassLoader:擴展類加載器
  • App ClassLoader:也稱爲SystemAppClass,系統類加載器

1.1 Bootstrap ClassLoader

Bootstrap ClassLoader用來加載JVM(Java虛擬機)運行時所須要的系統類,其使用c++實現。ide

從如下路徑來加載類:oop

  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的部分代碼以下:

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類中。

部分代碼以下:

  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是什麼,其背後又有什麼呢?

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

  • 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. 若是沒找到,則繼續依次向下查找,若是還沒找到則最後會交由自身去查找

(圖片來自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…

相關文章
相關標籤/搜索