【修煉內功】[JVM] 深刻理解JVM之ClassLoader

深刻理解JVM之ClassLoader

本文已收錄 【修煉內功】躍遷之路

Java在誕生之初便提出 "Write Once, Run Anywhere",各提供商發佈不少不一樣平臺的虛擬機,這些虛擬機均可以載入並執行同平臺無關的字節碼。java

設計者在初版Java虛擬機規範中便承諾 "In the future, we will consider bounded extensions to the Java virtual machine to provide better support for other languages",時至今日,商業機構和開源機構已在Java以外發展出一大批能夠在JVM上運行的語言,如Clojure、Groovy、JRuby、Jpython、Scala、Kotlin等。這些語言都不是本地可執行程序,須要JVM將編譯後的class文件加載後纔可以運行,負責加載class文件的組件即是ClassLoader.python

JVM類加載流程

java語言系統內置了衆多類加載器,從必定程度上講,只存在兩種不一樣的類加載器:一種是啓動類加載器,此類加載由C++實現,是JVM的一部分;另外一種就是全部其餘的類加載器,這些類加載器均由java實現,且所有繼承自java.lang.ClassLoadersegmentfault

  • Bootstrap ClassLoader 啓動類加載器,最頂層的加載類,由C++實現,負責加載%JAVA_HOME%/lib目錄中或-Xbootclasspath中參數指定的路徑中的,而且是虛擬機識別的(按名稱)類庫
  • Extention ClassLoader 擴展類加載器,由啓動類加載器加載,實現爲sun.misc.Launcher$ExtClassLoader,負責加載目錄%JRE_HOME%/lib/ext目錄中或-Djava.ext.dirs中參數指定的路徑中的jar包和class文件
  • Application ClassLoader 應用類加載器,也稱爲系統類加載器(System ClassLoader,可由java.lang.ClassLoader.getSystemClassLoader()獲取),實現爲sun.misc.Launcher$AppClassLoader,由啓動類加載器加載,負責加載當前應用classpath下的全部類

雙親委派模型

java語言系統有衆多類加載器,包括用戶自定義類加載器,各加載器之間的加載順序如何?首先從JVM入口應用sun.misc.Launcher聊起tomcat

Launcher

public Launcher() {
    ExtClassLoader localExtClassLoader;
    try {
        // 加載擴展類加載器
        localExtClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException localIOException1) {
        throw new InternalError("Could not create extension class loader", localIOException1);
    }
    try {
        // 加載應用類加載器
        this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    } catch (IOException localIOException2) {
        throw new InternalError("Could not create application class loader", localIOException2);
    }
    // 設置AppClassLoader爲線程上下文類加載器
    Thread.currentThread().setContextClassLoader(this.loader);
    // ...
    
    static class ExtClassLoader extends java.net.URLClassLoader
    static class AppClassLoader extends java.net.URLClassLoader
}

Launcher初始化了ExtClassLoaderAppClassLoader,並將AppClassLoader設置爲線程上下文類加載器,同時,初始化AppClassLoader時傳入了ExtClassLoader實例,WHY? 這裏要寫一個大大的問號網絡

ExtClassLoaderAppClassLoader都繼承自URLClassLoader,而最終的父類則爲ClassLoaderapp

classloader

查看源碼能夠得知,初始化AppClassLoader時傳入的ExtClassLoader實例最終設置爲了AppClassLoader(ClassLoader)的parent屬性,parent屬性的做用是什麼?jvm

父類加載器

每一個類都對應一個加載它的類加載器,咱們能夠經過程序來驗證ide

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("DNSNameService's ClassLoader is " + DNSNameService.class.getClassLoader());
        System.out.println("String's ClassLoader is " + String.class.getClassLoader());
    }
}

輸出爲測試

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@135fbaa4
DNSNameService's ClassLoader is sun.misc.Launcher$ExtClassLoader@6e0be858
String's ClassLoader is null

ClassLodarDemo爲咱們本身建立的類,其類加載器爲AppClassLoader
DNSNameService爲%JRE_HOME%/lib/ext目錄下的類,其類加載器爲ExtClassLoader
String存在於rt.jar中,但其類加載器爲null,這裏是應爲rt.jar由Bootstrap ClassLoader加載,而Bootstrap ClassLoader是由C++編寫,屬於JVM的一部分this

每一個類加載器,都有一個父類加載器(parent),一樣經過程序驗證

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

輸出爲

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@135fbaa4
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@2503dbd3
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父類加載器爲ExtClassLoader
ExtClassLoader的父類加載器爲null,null並不表明ExtClassLoader沒有父類加載器,而是Bootstrap ClassLoader

雙親委派

委派

類加載器在加載類或者其餘資源時,使用的是如上圖所示的雙親委派模型,這種模型要求除了頂層的BootStrap ClassLoader外,其他的類加載器都應當有本身的父類加載器(父類加載器不是父類繼承),若是一個類加載器收到了類加載請求,首先會把這個請求委派給父類加載器加載,只有父類加載器沒法完成類加載請求時,子類加載器纔會嘗試本身去加載。

要理解雙親委派,能夠查看ClassLoader.loadClass方法

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 thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 若是父類加載器沒法加載,才嘗試加載
                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的世界中大部分的類加載器都遵循這個模型,但也有例外,一個典型的例子即是JNDI服務。

JNDI存放於rt.jar,由啓動類加載器加載,JNDI的目的是對資源進行集中管理和查找,它須要調用由各廠商實現並部署在應用程序ClassPath下的JNDI接口實現的代碼,如此一來,在雙親委派模型下,啓動類加載器根本沒法加載這些代碼

針對此問題,java設計團隊引入了線程上下文類加載器(Thread Context ClassLoader),這個類加載器能夠經過Thread類的setContextClassLoader進行設置,默認繼承父線程類加載器
經過線程上下文類加載器,JNDI服務經過此類加載器,由父類加載器請求子類加載器完成類加載動做

破壞雙親委派模型的例子還有不少,如tomcat服務、osgi、jigsaw等等,是否破壞雙親委派模型並無對與錯,只是不一樣場景下的具體應用而已

自定義ClassLoader

不管是AppClassLoader仍是ExtClassLoader仍是啓動類加載器,其加載類的路徑都是固定的,若是咱們須要加載外部類或者資源,如某路徑下或網絡上,這樣便須要自定義類加載器
自定義類加載器,只須要繼承ClassLoader類,複寫findClass方法,在findClass方法中調用defineClass方法便可

一個ClassLoader建立時若是沒有指定parent,那麼它的parent默認就是AppClassLoader
若是須要制定一個ClassLoader的父類加載器爲啓動類加載器,只須要將其parent指定爲null便可

首先,編寫一個測試用的類文件

public class BeLoadedClass {
    public void say() {
        System.out.println("I'm Loaded by " + this.getClass().getClassLoader());
    }
}

將其編譯,放入/data/classloader目錄下

接下來,編寫DiskClassLoader

public class DiskClassLoader extends URLClassLoader {

    public DiskClassLoader(URL path) throws MalformedURLException {
        super(new URL[]{path});
    }

    public DiskClassLoader(URL path, ClassLoader parent) throws MalformedURLException {
        super(new URL[]{path}, parent);
    }
}

這裏直接繼承URLClassLoader類,該類findClass的實現以下

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 {
                            // 調用defineClass生成類
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

編寫測試程序

public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        URL path = new File("/data/classloader").toURI().toURL();

        DiskClassLoader diskClassLoaderA = new DiskClassLoader(path);
        Class<?> clazzA = diskClassLoaderA.loadClass("BeLoadedClass");
        Method sayA = clazzA.getMethod("say");
        Object instanceA = clazzA.newInstance();
        sayA.invoke(instanceA);
        System.out.println(diskClassLoaderA);
        System.out.println("clazzA@" + clazzA.hashCode());

        System.out.println("====");

        DiskClassLoader diskClassLoaderB = new DiskClassLoader(path, diskClassLoaderA);
        Class<?> clazzB = diskClassLoaderB.loadClass("BeLoadedClass");
        Method sayB = clazzB.getMethod("say");
        Object instanceB = clazzA.newInstance();
        sayB.invoke(instanceB);
        System.out.println(diskClassLoaderB);
        System.out.println("clazzB@" + clazzB.hashCode());

        System.out.println("====");

        DiskClassLoader diskClassLoaderC = new DiskClassLoader(path);
        Class<?> clazzC = diskClassLoaderC.loadClass("BeLoadedClass");
        Method sayC = clazzC.getMethod("say");
        Object instanceC = clazzC.newInstance();
        sayC.invoke(instanceC);
        System.out.println(diskClassLoaderC);
        System.out.println("clazzC@" + clazzC.hashCode());

        System.out.println("====");

        System.out.println("clazzA == clazzB " + (clazzA == clazzB));
        System.out.println("clazzC == clazzB " + (clazzC == clazzB));
    }
}

輸出爲

I'm Loaded by com.manerfan.jvm.oom.DiskClassLoader@4b67cf4d
com.manerfan.jvm.DiskClassLoader@4b67cf4d
clazzA@312714112
====
I'm Loaded by com.manerfan.jvm.oom.DiskClassLoader@4b67cf4d
com.manerfan.jvm.DiskClassLoader@29453f44
clazzB@312714112
====
I'm Loaded by com.manerfan.jvm.oom.DiskClassLoader@5cad8086
com.manerfan.jvm.DiskClassLoader@5cad8086
clazzC@1639705018
====
clazzA == clazzB true
clazzC == clazzB false

這裏咱們定義了3個ClassLoader,類搜索路徑均爲/data/classloader,其中diskClassLoaderB的父類加載器爲diskClassLoaderA
clazzA clazzB clazzC 分別由 diskClassLoaderA diskClassLoaderB diskClassLoaderC 加載
instanceA instanceB instanceA 分別由 clazzA clazzB clazzC 建立

從輸出能夠看出,instanceA及instanceB所對應的class均由diskClassLoaderA加載
雖然diskClassLoaderA與diskClassLoaderB爲兩個不一樣的類加載器,但因爲diskClassLoaderB的父類加載器爲diskClassLoaderA,從輸出結果能夠看出,clazzA與clazzB徹底相同(包括內存地址),這也說明clazzA與clazzB均由同一類加載器加載
而instanceC所對應的class,classC倒是由diskClassLoaderC加載

這個例子,能更好的幫助理解雙親委派模型

類的惟一性

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間

如上例,雖然classA(classB)與classC的類路徑相同,但因爲被不一樣的類加載器加載,其卻屬於兩個不一樣的類名稱空間


訂閱號

相關文章
相關標籤/搜索