Java類加載器(ClassLoader)

首先需要了解一下類的加載過程。
類加載器的任務是根據(jù)一個類的全限定名來讀取此類的二進制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個與目標類對應的java.lang.Class對象實例

在虛擬機提供了3種類加載器,引導(Bootstrap)類加載器擴展(Extension)類加載器系統(tǒng)(System)類加載器(也稱應用類加載器)

1. 啟動(Bootstrap)類加載器

啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現(xiàn)的,是虛擬機自身的一部分

它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類)。

2 擴展(Extension)類加載器

擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader類,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫,開發(fā)者可以直接使用標準擴展類加載器

3 系統(tǒng)(System)類加載器

也稱應用程序加載器是指 Sun公司實現(xiàn)的sun.misc.Launcher$AppClassLoader。它負責加載系統(tǒng)類路徑java -classpath-D java.class.path 指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。

應用程序類加載器,該加載器是由sun.misc.Launcher$AppClassLoader實現(xiàn),該類加載器負責加載用戶類路徑上所指定的類庫。開發(fā)者可通過ClassLoader.getSystemClassLoader()方法直接獲取,故又稱為系統(tǒng)類加載器。當應用程序沒有自定義類加載器時,默認采用該類加載器

ClassLoader.java

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader(); //初始化系統(tǒng)類加載器 【見下文】
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader ccl = getCallerClassLoader();
        if (ccl != null && ccl != scl && !scl.isAncestor(ccl)) {
            sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
        }
    }
    return scl;
}

系統(tǒng)類加載器初始化:

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

2 雙親委派模型

java雙親委派模型

4 類加載器間的關系

我們進一步了解類加載器間的關系(并非指繼承關系),主要可以分為以下4點

  • 啟動類加載器,由C++實現(xiàn),沒有父類。
  • 拓展類加載器(ExtClassLoader),由Java語言實現(xiàn),父類加載器為null
  • 系統(tǒng)類加載器(AppClassLoader),由Java語言實現(xiàn),父類加載器為ExtClassLoader
  • 自定義類加載器,父類加載器肯定為AppClassLoader。

5 兩個class對象是否為同一個類對象

在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件

  • 類的完整類名必須一致,包括包名。
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。

也就是說,在JVM中,即使這個兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間,所以加載的class對象也會存在不同的類名空間中

但前提是覆寫loadclass方法,從前面雙親委派模式對loadClass()方法的源碼分析中可以知,在方法第一步會通過Class<?> c = findLoadedClass(name);從緩存查找,類名完整名稱相同則不會再次被加載,因此我們必須繞過緩存查詢才能重新加載class對象。當然也可直接調(diào)用findClass()方法,這樣也避免從緩存查找,如下

String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//創(chuàng)建兩個不同的自定義類加載器實例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//通過findClass創(chuàng)建類的Class對象
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");

System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());

/**
  * 直接調(diào)用findClass方法輸出結果:
  * findClass->obj1:723074861
    findClass->obj2:895328852
    生成不同的實例
  */

如果調(diào)用父類的loadClass方法,結果如下,除非重寫loadClass()方法去掉緩存查找步驟,不過現(xiàn)在一般都不建議重寫loadClass()方法。

//直接調(diào)用父類的loadClass()方法
Class<?> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class<?> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");

//不同實例對象的自定義類加載器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系統(tǒng)類加載器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());

/**
* 直接調(diào)用loadClass方法的輸出結果,注意并沒有重寫loadClass方法
* loadClass->obj1:1872034366
  loadClass->obj2:1872034366
  Class->    obj3:1872034366
  都是同一個實例
*/

所以如果不從緩存查詢相同完全類名的class對象,那么只有ClassLoader的實例對象不同,同一字節(jié)碼文件創(chuàng)建的class對象自然也不會相同。

6 class文件的顯示加載與隱式加載的概念

所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式,
顯示加載指的是在代碼中通過調(diào)用ClassLoader加載class對象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加載class對象。
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象,而是通過虛擬機自動加載到內(nèi)存中,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象,此時額外引用的類將通過JVM自動加載到內(nèi)存中。

3 自定義類加載器

  • 每一個ClassLoader都擁有自己獨立的類名稱空間,類是由ClassLoader將其加載到Java虛擬機中,故類是由加載它的ClassLoader和該類本身一起確定其在Java 運行時環(huán)境的唯一性。
  • 只有同一個ClassLoader加載的同一個類,才能算是Java 運行時環(huán)境中的相同的兩個類。哪怕是來自同一個Class文件,即使被同一個虛擬機加載的兩個類,只要ClassLoader不同,那么也屬于不同的類。
  • 對于equals()、isinstanceof()等方法來判斷對象的相等或所屬關系都是需要基于同一個ClassLoader。

自定義類加載器示例:

public class ClassLoadDemo{

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

        ClassLoader clazzLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String clazzName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    InputStream is = getClass().getResourceAsStream(clazzName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        String currentClass = "com.yuanhh.classloader.ClassLoadDemo";
        Class<?> clazz = clazzLoader.loadClass(currentClass);
        Object obj = clazz.newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof com.yuanhh.classloader.ClassLoadDemo);
    }
}

上面代碼的輸出結果:

class com.yuanhh.classloader.ClassLoadDemo
false
  • 輸出結果的第一行,可以看出這個對象的確是com.yuanhh.classloader.ClassLoadDemo實例化的對象;
  • 但第二句是false,這是由于代碼中的obj是由用戶自定義的類加載器clazzLoader來加載的,可通過obj.getClass().getClassLoader()獲取該對象的類加載器為com.yuanhh.classloader.ClassLoadDemoxxx,而虛擬機本身會由系統(tǒng)類加載器加載的類ClassLoadDemo,可通過ClassLoadDemo.class.getClassLoader()得其類加載器為sun.misc.LauncherAppClassLoader@XXX。
  • 所以可得出結論:即使都是來自同一個Class文件,加載器不同,仍然是兩個不同的類,所以返回值是false。
  • 通過Class.forName()方法加載的類,采用的是系統(tǒng)類加載器。

4 應用場景

  • Tomcat,類加載器架構,自己定義了多個類加載器,
    • 保證了同一個服務器的兩個Web應用程序的Java類庫隔離;
    • 保證了同一個服務器的兩個Web應用程序的Java類庫又可以相互共享;比如多個Spring組織的應用程序不能共享,會造成資源浪費;
    • 保證了服務器盡可能保證自身的安全不受不受部署Web應用程序影響;
    • 支持JSP應用的服務器,大多需要支持熱替換(HotSwap)功能。
  • OSGi(Open Service GateWay Initiative),是基于Java語言的動態(tài)模塊化規(guī)范。已成為Java世界的“事實上”的模塊化標準,最為熟悉的案例的Eclipse IDE。

參考

Java類加載器(ClassLoader)
深入理解Java類加載器(ClassLoader)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容