本文首發於我的網站,如需轉載請註明來源:類加載器中的雙親委派模型,看這篇就夠了html
在上一篇文章中,咱們梳理了類加載器的基本概念:類的生命週期、類加載器的做用、類的加載和卸載的時機等等,這篇文章咱們接着前文繼續複習類加載器的知識,主要包括:JVM中有哪些類加載器?它們之間是什麼關係?什麼是雙親委派機制?java
從JVM的角度看,類加載器主要有兩類:Bootstrap ClassLoader和其餘類加載,Bootstrap ClassLoader是C++語言實現,是虛擬機自身的一部分;其餘類加載器都是Java語言實現,不屬於虛擬機,所有繼承自抽象類java.lang.ClassLoader。mysql
從Java開發者的角度看,須要瞭解類加載器的雙親委派模型,以下圖所示:面試
Bootstrap ClassLoader:啓動類加載器,這個類加載器將負責存放在
Extension ClassLoader:擴展類加載器,這個類加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載
Application ClassLoader:應用程序類加載器,這個類加載器由sun.misc.Launcher$AppClassLoader實現,它負責加載用戶CLASSPATH環境變量指定的路徑中的全部類庫。若是應用程序中沒有自定義過本身的類加載器,這個就是一個Java程序中默認的類加載器。後端
用戶自定義的類加載器:用戶在須要的狀況下,能夠實現本身的自定義類加載器,通常而言,在如下幾種狀況下須要自定義類加載器:(1)隔離加載類。某些框架爲了實現中間件和應用程序的模塊的隔離,就須要中間件和應用程序使用不一樣的類加載器;(2)修改類加載的方式。類加載的雙親委派模型並非強制的,用戶能夠根據須要在某個時間點動態加載類;(3)擴展類加載源,例如從數據庫、網絡進行類加載;(4)防止源代碼泄露。Java代碼很容易被反編譯和篡改,爲了防止源碼泄露,能夠對類的字節碼文件進行加密,並編寫自定義的類加載器來加載本身的應用程序的類。網絡
在下面的代碼中,java.util.HashMap是rt.jar包中的類,所以它的類加載器是null,DNSNameService類是放在ext目錄下的jar包中的類,所以它的類加載器是ExtClassLoader;MyClassLoaderTest的類加載器就是應用類加載器。app
import java.util.HashMap; import sun.net.spi.nameservice.dns.DNSNameService; public class MyClassLoaderTest { public static void main(String[] args) { System.out.println("class loader for HashMap: " + HashMap.class.getClassLoader()); System.out.println( "class loader for DNSNameService: " + DNSNameService.class.getClassLoader()); System.out.println("class loader for this class: " + MyClassLoaderTest.class.getClassLoader()); System.out.println("class loader for Blob class: " + com.mysql.jdbc.Blob.class.getClassLoader()); } }
運行上述代碼的接入過下圖所示:框架
經過下面的這個程序,能夠看到,每一個類加載器負責的jar文件路徑都不同:
public class JVMClassLoader { public static void main(String[] args) { System.out.println("引導類加載器加載路徑:" + System.getProperty("sun.boot.class.path")); System.out.println("擴展類加載器加載路徑:" + System.getProperty("java.ext.dirs")); System.out.println("系統類加載器加載路徑:" + System.getProperty("java.class.path")); } }
Arthas中提供了classloader命令,能夠用來查看當前應用中的類加載器相關的統計信息,以下圖所示,
若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。相反,若是沒有使用雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身編寫了一個稱爲java.lang.Object的類,並放在程序的Class Path中,那系統中將會出現多個不一樣的Object類,Java類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂。
雙親委派模型的實現很是簡單,實現雙親委派的代碼在java.lang.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, // 說明父類加載器沒法完成加載請求 } if (c == null) { // 在父類加載器沒法加載的時候,再調用本類的findClass方法進行類加載請求 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats // 當前類加載器是該類的define class loader sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
如上所述,雙親委派模型很好得解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),若是基礎類又要回調用戶的類該怎麼辦?一個很是經典的例子就是SQL的驅動管理類——java.sql.DriverManager。
java.sql.DriverManager是Java的標準服務,該類放在rt.jar中,所以是由啓動類加載器加載的,可是在應用啓動的時候,該驅動類管理是須要加載由不一樣數據庫廠商實現的驅動,可是啓動類加載器找不到這些具體的實現類,爲了解決這個問題,Java設計團隊提供了一個不太優雅的設計:線程上下文加載器(Thread Context ClassLoader)。這個類加載器能夠經過java.lang.Thread類的setContextClassLoader()方法進行設置,若是建立線程時候它尚未被設置,就會從父線程中繼承一個,若是再應用程序的全局範圍都沒有設置過的話,那這個類加載器就是應用程序類加載器。
有了線程上下文加載器,就能夠解決上面的問題——父類加載器須要請求子類加載器完成類加載的動做,這種行爲實際上就是打破了雙親委派的加載規則。
接下來,咱們以java.sql.DriverManager爲例,看下線程上下文加載器的用法,在java.sql.DriverManager類的下面這個靜態塊中,是JDBC驅動加載的入口。
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
順着loadInitialDrivers()方法往下看,使用線程上下文加載器的地方在ServiceLoader.load裏
private static void loadInitialDrivers() { // ……省去別的代碼 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); //…… 省去別的代碼
ServiceLoader.load方法的代碼以下,JDBC的sqlDriverManager就是這裏得到的上下文加載器來驅動用戶代碼加載指定的類的。
public static <S> ServiceLoader<S> load(Class<S> service) { // 獲取當前線程中的上下文類加載器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
那麼這個上下文加載器是何時設置進去的呢?前面咱們提到了:
這個類加載器能夠經過java.lang.Thread類的setContextClassLoader()方法進行設置,若是建立線程時候它尚未被設置,就會從父線程中繼承一個,若是再應用程序的全局範圍都沒有設置過的話,那這個類加載器就是應用程序類加載器。
看下setContextClassLoader()方法別誰調用了,最終咱們在Launcher中找到了以下代碼:
public class 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); } Thread.currentThread().setContextClassLoader(this.loader); //……省去別的代碼 } }
這篇文章咱們複習了類加載器的雙親委派模型、雙親委派模型的工做過程,以及打破雙親委派模型的必要性和源碼分析。在第一部分的結尾,咱們還演示了Arthas中關於類加載器的命令的用法,在實際排查問題時能夠考慮使用。
本號專一於後端技術、JVM問題排查和優化、Java面試題、我的成長和自我管理等主題,爲讀者提供一線開發者的工做和成長經驗,期待你能在這裏有所收穫。