聊聊ClassLoader

一、什麼是類加載器

虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛 機外部實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的模塊稱爲「類加載器」。java

周志明. 深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版) 機械工業出版社.spring

二、須要注意的點

兩個類是「相等」(包括equals、isAssignableFrom、isInstanceOf)的前提條件是這兩個類的類加載器相等。sql

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {

    URL jar = new URL("file:\\G:\\code\\demo\\demo-0.0.1-SNAPSHOT.jar");
    URL[] urls = new URL[]{jar};

    //類加載器1
    URLClassLoader classLoader1 = new URLClassLoader(urls,null);
    Class userClass1 = classLoader1.loadClass("com.demo.User");

    //類加載器2
    URLClassLoader classLoader2 = new URLClassLoader(urls,null);
    Class userClass2 = classLoader2.loadClass("com.demo.User");

    //輸出false,緣由:userClass來自不一樣的類加載器
    System.out.println(userClass1.equals(userClass2));
}

三、類加載器的分類

  • 啓動類加載器(BootstrapClassLoader):前面已經介紹過,這個類將器負責將存放在<JAVA_HOME>\lib目錄中的,或 者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給引導類加載器,那直接使用null代替便可。
  • 擴展類加載器(ExtensionClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器(ApplicationClassLoader):這個類加載器由sun.misc.Launcher$App-ClassLoader實現。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。數據庫

    周志明. 深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版) 機械工業出版社tomcat

四、類加載器的雙親委託加載

ClassLoader的結構中有一個重要的成員變量parent,也就是咱們所說的ClassLoader的雙親。微信

// java.lang.ClassLoader
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ...

委派ClassLoader進行類加載的過程應該是:oracle

  • 首先判斷類是否已經加載,若是已經加載直接返回已加載的類
  • 若是沒有加載交給parent進行加載,若是加載成功返回類
  • 若是parent加載失敗,本身嘗試加載
    JDK中loadClass的過程以下:
// java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先判斷類是否已經加載,若是已經加載直接返回已加載的類
        Class<?> c = findLoadedClass(name); 
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 若是沒有加載交給parent進行加載,若是加載成功返回類
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 若是parent=null時,認爲parent=啓動類加載器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 若是parent加載失敗,本身嘗試加載
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }

檢查與加載過程如圖所示:
圖1app

五、雙親委託模式的弊端

要說明弊端,必須引入SPI。框架

什麼是SPI

SPI ,全稱爲 Service Provider Interface,是一種服務發現機制。JAVA中定義的SPI通常是要第三方進行實現,咱們比較常見的如:java.sql.Driver,JDK中只定義了Driver接口,並無去實現,Driver的實現由數據庫廠商來實現。
oralce數據庫驅動的實現以下:(來自:ojdbc6-11.2.0.4.0.jar)ide

public class OracleDriver implements Driver {
    ...
}

同時第三方jar必須增長配置文件:圖2.png
java.sql.Driver文件內容:oracle.jdbc.OracleDriver
java虛擬機經過掃描jar包下的配置文件信息加載對應接口的實現類。

SPI小示例

定義SayHello接口

package com.demo;
public interface SayHello {
    void hello();
}

實現SayHello接口

package com.demo;
public class SayHelloImpl implements SayHello {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

在META-INF/services目錄下增長com.demo.SayHello文件,文件內容爲:com.demo.SayHelloImpl
主函數

public class ClassLoaderApplication {
    public static void main(String[] args) {
        ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
        for (SayHello s : sayHellos) {
            s.hello();
        }
    }
}

SPI引入給雙親委託模式帶來的衝擊

以java.sql.Driver爲例,java.sql.Driver接口定義在rt.jar中,而rt.jar由BootstrapClassLoader負責加載,Driver最終由同在rt.jar包中的DriverManager類所使用,代碼以下:

// class : DriverManager
private static void loadInitialDrivers() {
    ...
     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    ...

因爲DriverManager類在rt.jar中,因此能夠認定DriverManager類最終由BootstrapClassLoader加載器負責加載,而咱們的Driver實現類(OracleDriver)通常都是由應用程序類加載器(ApplicationClassLoader)或自定義類加載器負責加載,因此Driver的實現對BootstrapClassLoader是不可見的,這樣一定會致使DriverManager的loadInitialDrivers失敗。

解決方案

爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(ThreadContextClassLoader)。這個類加載器能夠經過java.lang.Thread類的setContextClassLoaser()方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
看下ServiceLoader的相關源碼:

//class : ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    //ServiceLoader就是經過Thread.currentThread().getContextClassLoader()獲取類加載器的
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

因此解決DriverManager類中能夠加載OracleDriver的問題,能夠經過將應用程序類加載器(ApplicationClassLoader)設置到java.lang.Thread類的setContextClassLoaser()方法來解決。
其實這一過程咱們基本不用本身來敲代碼實現,由於咱們用的容器都已經幫咱們考慮到了。以tomcat(9.0.24)的源碼爲例:

//class : WebappLoader
@Override
public void backgroundProcess() {
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
  }

六、再來聊聊Spring中的ClassLoader

咱們定義的JavaBean在spring的getBean方法的建立過程其實與DriverManager建立Driver實例的過程是同樣的。咱們的JavaBean是通常都是由應用程序類加載器(ApplicationClassLoader)或自定義類加載器負責加載,而Spring作爲一款開源框架多是有更高層類加載器負責加載,因此Spring獲取JavaBean的Class時第一優先級是經過Thread.currentThread().getContextClassLoader()來獲取JavaBean的Class的類加載器。如代碼所示:

//org.springframework.util.ClassUtils
public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;

    try {
        cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable var3) {
    }

    if (cl == null) {
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            try {
                cl = ClassLoader.getSystemClassLoader();
            } catch (Throwable var2) {
            }
        }
    }

    return cl;
}

一、什麼是類加載器

虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛 機外部實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的模塊稱爲「類加載器」。

周志明. 深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版) 機械工業出版社.

二、須要注意的點

兩個類是「相等」(包括equals、isAssignableFrom、isInstanceOf)的前提條件是這兩個類的類加載器相等。

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {

    URL jar = new URL("file:\\G:\\code\\demo\\demo-0.0.1-SNAPSHOT.jar");
    URL[] urls = new URL[]{jar};

    //類加載器1
    URLClassLoader classLoader1 = new URLClassLoader(urls,null);
    Class userClass1 = classLoader1.loadClass("com.demo.User");

    //類加載器2
    URLClassLoader classLoader2 = new URLClassLoader(urls,null);
    Class userClass2 = classLoader2.loadClass("com.demo.User");

    //輸出false,緣由:userClass來自不一樣的類加載器
    System.out.println(userClass1.equals(userClass2));
}

三、類加載器的分類

  • 啓動類加載器(BootstrapClassLoader):前面已經介紹過,這個類將器負責將存放在<JAVA_HOME>\lib目錄中的,或 者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給引導類加載器,那直接使用null代替便可。
  • 擴展類加載器(ExtensionClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器(ApplicationClassLoader):這個類加載器由sun.misc.Launcher$App-ClassLoader實現。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

    周志明. 深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版) 機械工業出版社

四、類加載器的雙親委託加載

ClassLoader的結構中有一個重要的成員變量parent,也就是咱們所說的ClassLoader的雙親。

// java.lang.ClassLoader
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ...

委派ClassLoader進行類加載的過程應該是:

  • 首先判斷類是否已經加載,若是已經加載直接返回已加載的類
  • 若是沒有加載交給parent進行加載,若是加載成功返回類
  • 若是parent加載失敗,本身嘗試加載
    JDK中loadClass的過程以下:
// java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先判斷類是否已經加載,若是已經加載直接返回已加載的類
        Class<?> c = findLoadedClass(name); 
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 若是沒有加載交給parent進行加載,若是加載成功返回類
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 若是parent=null時,認爲parent=啓動類加載器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            
            // 若是parent加載失敗,本身嘗試加載
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }

檢查與加載過程如圖所示:
圖1

五、雙親委託模式的弊端

要說明弊端,必須引入SPI。

什麼是SPI

SPI ,全稱爲 Service Provider Interface,是一種服務發現機制。JAVA中定義的SPI通常是要第三方進行實現,咱們比較常見的如:java.sql.Driver,JDK中只定義了Driver接口,並無去實現,Driver的實現由數據庫廠商來實現。
oralce數據庫驅動的實現以下:(來自:ojdbc6-11.2.0.4.0.jar)

public class OracleDriver implements Driver {
    ...
}

同時第三方jar必須增長配置文件:圖2.png
java.sql.Driver文件內容:oracle.jdbc.OracleDriver
java虛擬機經過掃描jar包下的配置文件信息加載對應接口的實現類。

SPI小示例

定義SayHello接口

package com.demo;
public interface SayHello {
    void hello();
}

實現SayHello接口

package com.demo;
public class SayHelloImpl implements SayHello {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

在META-INF/services目錄下增長com.demo.SayHello文件,文件內容爲:com.demo.SayHelloImpl
主函數

public class ClassLoaderApplication {
    public static void main(String[] args) {
        ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
        for (SayHello s : sayHellos) {
            s.hello();
        }
    }
}

SPI引入給雙親委託模式帶來的衝擊

以java.sql.Driver爲例,java.sql.Driver接口定義在rt.jar中,而rt.jar由BootstrapClassLoader負責加載,Driver最終由同在rt.jar包中的DriverManager類所使用,代碼以下:

// class : DriverManager
private static void loadInitialDrivers() {
    ...
     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    ...

因爲DriverManager類在rt.jar中,因此能夠認定DriverManager類最終由BootstrapClassLoader加載器負責加載,而咱們的Driver實現類(OracleDriver)通常都是由應用程序類加載器(ApplicationClassLoader)或自定義類加載器負責加載,因此Driver的實現對BootstrapClassLoader是不可見的,這樣一定會致使DriverManager的loadInitialDrivers失敗。

解決方案

爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(ThreadContextClassLoader)。這個類加載器能夠經過java.lang.Thread類的setContextClassLoaser()方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
看下ServiceLoader的相關源碼:

//class : ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
    //ServiceLoader就是經過Thread.currentThread().getContextClassLoader()獲取類加載器的
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

因此解決DriverManager類中能夠加載OracleDriver的問題,能夠經過將應用程序類加載器(ApplicationClassLoader)設置到java.lang.Thread類的setContextClassLoaser()方法來解決。
其實這一過程咱們基本不用本身來敲代碼實現,由於咱們用的容器都已經幫咱們考慮到了。以tomcat(9.0.24)的源碼爲例:

//class : WebappLoader
@Override
public void backgroundProcess() {
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
  }

六、再來聊聊Spring中的ClassLoader

咱們定義的JavaBean在spring的getBean方法的建立過程其實與DriverManager建立Driver實例的過程是同樣的。咱們的JavaBean是通常都是由應用程序類加載器(ApplicationClassLoader)或自定義類加載器負責加載,而Spring作爲一款開源框架多是有更高層類加載器負責加載,因此Spring獲取JavaBean的Class時第一優先級是經過Thread.currentThread().getContextClassLoader()來獲取JavaBean的Class的類加載器。如代碼所示:

//org.springframework.util.ClassUtils
public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;

    try {
        cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable var3) {
    }

    if (cl == null) {
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            try {
                cl = ClassLoader.getSystemClassLoader();
            } catch (Throwable var2) {
            }
        }
    }

    return cl;
}

更多spring源碼相關知識點擊
《超哥spring源碼解析之核心容器篇》免費視頻學習
也能夠關注超哥微信公衆號:
超哥spring源碼解析.jpg

相關文章
相關標籤/搜索