Java類加載器與雙親委派機制和線程上下文類加載器專項解讀分析
一、類加載器
類加載器就是根據類的二進制名(binary name)讀取java編譯器編譯好的字節碼文件(.class文件),並且轉化生成一個java.lang.Class類的一個實例。
每個實例用來表示一個Java類,jvm就是用這些實例來生成java對象的。
如new一個String對象;反射生成一個String對象,都會用到String.class 這個java.lang.Class類的對象。
基本上所有的類加載器都是java.lang.ClassLoader 類的一個實例
類加載器3大分類
類加載器 | 加載類 | 說明 |
啟動類加載器(Bootstrap ClassLoader) | JAVA_HOME/jre/lib | 無法直接訪問 |
拓展類加載器(Extension ClassLoader) | JAVA_HOME/jre/lib/ext | 上級為 Bootstrap,顯示為 null |
應用類加載器(Application ClassLoader) | classpath | 上級為 Extension |
自定義類加載器 | 自定義 | 上級為 Application |
類加載器加載順序如下
類加載器的核心方法
方法名 | 說明 |
---|---|
getParent() | 返回該類加載器的父類加載器 |
loadClass(String name) | 加載名為name的類,返回java.lang.Class類的實例 |
findClass(String name) | 查找名字為name的類,返回的結果是java.lang.Class類的實例 |
findLoadedClass(String name) | 查找名字為name的已經被加載過的類,返回的結果是java.lang.Class類的實例 |
defineClass(String name,byte[] b,int off,int len) | 根據字節數組b中的數據轉化成Java類,返回的結果是java.lang.Class類的實例 |
上述方法的name參數都是binary name(類的二進制名字),如
java.lang.String <包名>.<類名>
java.concurrent.locks.AbstractQueuedSynchronizer$Node <包名>.<類名>$<內部類名>
java.net.URLClassLoader$1 <包名>.<類名>.<匿名內部類名>
1.啟動類加載器
啟動類加載器是jvm在運行時,內嵌在jvm中的一段特殊的用來加載java核心類庫的C++代碼
String.class 對象就是由啟動類加載器加載的,啟動類加載器具體加載哪些核心代碼可以通過獲取值為 "sun.boot.class.path" 的系統屬性獲得。
啟動類加載器不是java原生代碼編寫的,所以其也不是java.lang.ClassLoader類的實例,其沒有getParent方法
public static void main(String[] args) throws ClassNotFoundException { Class<?> aClass = Class.forName("com.mycompany.load.F"); System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader }
輸出
cd D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm
D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm>java -Xbootclasspath/a:. com.mycompany.load.Load4
bootstrap F init
null
打印 null,表示它的類加載器是 Bootstrap ClassLoader
-Xbootclasspath 表示設置 bootclasspath
其中 /a:. 表示將當前目錄追加至 bootclasspath 之後
用這個辦法替換核心類
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<後追加路徑>
java -Xbootclasspath/p:<前追加路徑>
2.拓展類加載器
拓展類加載器用來加載jvm實現的一個拓展目錄,該目錄下的所有java類都由此類加載器加載。
此路徑可以通過獲取"java.ext.dirs"的系統屬性獲得。拓展類加載器就是java.lang.ClassLoader類的一個實例,其getParent方法返回的是引導類加載器(在 HotSpot虛擬機中用null表示引導類加載)
D:\Java\JavaProject\jvm-demo\myjvm\out\production\myjvm>jar -cvf my.jar com/mycompany/load/F.class
已添加清單
正在添加: com/mycompany/load/F.class(輸入 = 481) (輸出 = 322)(壓縮瞭 33%)
將 jar 包拷貝到 JAVA_HOME/jre/lib/ext,重新執行代碼即可
3.應用類加載器
應用類加載器又稱為系統類加載器,開發者可用通過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類加載器的實例,系統類加載器也因此得名。其主要負責加載程序開發者自己編寫的java類
一般來說,java應用都是用此類加載器完成加載的,可以通過獲取"java.class.path"的系統屬性(也就是我們常說的classpath)來獲取應用類加載器加載的類路徑。應用類加載器是java.lang.ClassLoader類的一個實例,其getParent方法返回的是拓展類加載器
4.類的命名空間
在程序運行過程中,一個類並不是簡單由其二進制名字(binary name)定義的,而是通過其二進制名和其定義加載器所確定的命名空間(run-time package)所共同確定的。
同一個二進制名的類由不同的定義加載器加載時,其返回的Class對象不是同一個,那麼由不同的Class對象所創建的對象,其類型也不是相同的。
類似Test cannot be cast to Test的java.lang.ClassCastException 的奇怪錯誤很多情況下都是類的二進制名相同,而定義加載器不同造成的
package com.mycompany.load; import sun.misc.Launcher; public class Load6 { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader classLoader = new Launcher().getClassLoader(); //1 new一個新的類加載器 System.out.println(classLoader); /* 這是因為 1處獲取的應用類加載器a和jvm用來加載Load6.class對象的應用類加載器b不是同一個實例, 那麼構成這兩個類的run-time package也就是不同的。所以即使它們的二進制名字相同, 但是由a定義的Load6類所創建的對象顯然不能轉化為由b定義的Load6類的實例。 這種情況下jvm就會拋出ClassCastException * */ Class<?> aClass = classLoader.loadClass("com.mycompany.load.Load6"); Load6 load6 = (Load6)aClass.newInstance(); //2 //Exception in thread "main" java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6 } }
報出異常:
java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6
這是因為 1處獲取的應用類加載器a和jvm用來加載Load6.class對象的應用類加載器b不是同一個實例, 那麼構成這兩個類的run-time package也就是不同的。
所以即使它們的二進制名字相同, 但是由a定義的Load6類所創建的對象顯然不能轉化為由b定義的Load6類的實例。 這種情況下jvm就會拋出ClassCastException
相同二進制名字的類,如果其定義加載器不同,也算是不同的兩個類
二、雙親委派機制
雙親委派機制 Parent Delegation Model,又稱為父級委托模型。所謂的雙親委派,就是指調用類加載器的 loadClass 方法時,查找類的規則(雙親,理解為上級更為合適,因為它們之間並沒有繼承關系)
1.類加載機制流程
Java編譯器把Java源文件編譯成.class文件,再由JVM裝載.class文件到內存中,JVM裝載完成後得到一個Class對象字節碼。有瞭字節碼對象,就可以實例化使用
2.類加載器加載順序
3.雙親委派機制流程
1、加載類MyClass.class,從低層級到高層級一級一級委派,先由應用層加載器委派給擴展類加載器,再由擴展類委派給啟動類加載器
(1)如果是自定義加載器掛載到應用程序類加載器
(2)應用程序類加載器將類加載請求委托給擴展類加載器
(3)擴展類加載器將類加載請求委托給啟動類加載器
2、啟動類加載器載入失敗,再由擴展類加載器載入,擴展類加載器載入失敗,最後由應用類加載器載入
(1)啟動類加載器在加載路徑下查找並加載Class文件,如果未找到目標Class文件,則交由擴展類加載器加載
(2)擴展類加載器在加載路徑下查找並加載Class文件,如果未找到目標Class文件,則交由應用程序類加載器加載
(3)應用程序類加載器在加載路徑下查找並加載Class文件,如果未找到目標Class文件,則交由自定義加載器加載
(4)在自定義加載器下查找並加載用戶指定目錄下的Class文件,如果在自定義加載路徑下未找到目標Class文件,則拋出ClassNotFoud異常
3、如果應用類加載器也找不到那就報ClassNotFound異常瞭
4.源碼分析
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 檢查該類是否已經加載 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 2. 有上級的話,委派上級 loadClass c = parent.loadClass(name, false); } else { // 3. 如果沒有上級瞭(ExtClassLoader),則委派 BootstrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); // 4. 每一層找不到,調用 findClass 方法(每個類加載器自己擴展)來加載 c = findClass(name); // 5. 記錄耗時 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、sun.misc.Launcher$AppClassLoader //1 處, 開始查看已加載的類,如果沒有
2、sun.misc.Launcher$AppClassLoader // 2 處,委派上級 sun.misc.Launcher$ExtClassLoader.loadClass()
3、sun.misc.Launcher$ExtClassLoader // 1 處,查看已加載的類,如果沒有
4、sun.misc.Launcher$ExtClassLoader // 3 處,沒有上級瞭,則委派 BootstrapClassLoader 查找
5、BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 類,沒有
6、sun.misc.Launcher$ExtClassLoader // 4 處,調用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 類,顯然沒有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 處
7、繼續執行到 sun.misc.Launcher$AppClassLoader // 4 處,調用它自己的 findClass 方法,在 classpath 下查找,找到瞭
5.雙親委派機制優缺點
優點:
1、保證安全性,層級關系代表優先級,也就是所有類的加載,優先給啟動類加載器,這樣就保證瞭核心類庫類
2、避免類的重復加載,如果父類加載器加載過瞭,子類加載器就沒有必要再去加載瞭,確保一個類的全局唯一性
缺點:
檢查類是否加載的委派過程是單向的, 這個方式雖然從結構上說比較清晰,使各個 ClassLoader 的職責非常明確, 但是同時會帶來一個問題, 即頂層的ClassLoader 無法訪問底層的ClassLoader 所加載的類
通常情況下, 啟動類加載器中的類為系統核心類, 包括一些重要的系統接口,而在應用類加載器中, 為應用類。 按照這種模式, 應用類訪問系統類自然是沒有問題, 但是系統類訪問應用類就會出現問題。
如在系統類中提供瞭一個接口, 該接口需要在應用類中得以實現, 該接口還綁定一個工廠方法, 用於創建該接口的實例, 而接口和工廠方法都在啟動類加載器中。 這時, 就會出現該工廠方法無法創建由應用類加載器加載的應用實例
三、線程上下文類加載器
線程上下文類加載器就是用來解決類的雙親委托模型的缺陷
在Java中,官方為我們提供瞭很多SPI接口,例如JDBC、JBI、JNDI等。這類SPI接口,官方往往隻會定義規范,具體的實現則是由第三方來完成的,比如JDBC,不同的數據庫廠商都需自己根據JDBC接口的定義進行實現。
而這些SPI接口直接由Java核心庫來提供,一般位於rt.jar
包中,而第三方實現的具體代碼庫則一般被放在classpath
的路徑下。而此時問題來瞭:
位於rt.jar
包中的SPI接口,是由Bootstrap類加載器完成加載的,而classpath
路徑下的SPI實現類,則是App
類加載器進行加載的。
但往往在SPI接口中,會經常調用實現者的代碼,所以一般會需要先去加載自己的實現類,但實現類並不在Bootstrap類加載器的加載范圍內,經過前面的雙親委派機制的分析,我們已經得知:子類加載器可以將類加載請求委托給父類加載器進行加載,但這個過程是不可逆的。也就是父類加載器是不能將類加載請求委派給自己的子類加載器進行加載的,
此時就出現瞭這個問題:如何加載SPI接口的實現類?那就是打破雙親委派模型
SPI(Service Provider Interface):Java的SPI機制,其實就是可拔插機制。在一個系統中,往往會被分為不同的模塊,比如日志模塊、JDBC模塊等,而每個模塊一般都存在多種實現方案,如果在Java的核心庫中,直接以硬編碼的方式寫死實現邏輯,那麼如果要更換另一種實現方案,就需要修改核心庫代碼,這就違反瞭可拔插機制的原則。為瞭避免這樣的問題出現,就需要一種動態的服務發現機制,可以在程序啟動過程中,動態的檢測實現者。而SPI中就提供瞭這麼一種機制,專門為某個接口尋找服務實現的機制。如下:
當第三方實現者提供瞭服務接口的一種實現之後,在jar包的 META-INF/services/ 目錄裡同時創建一個以服務接口命名的文件,該文件就是實現該服務接口的實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包 META-INF/services/ 裡的配置文件找到具體的實現類名,並裝載實例化,完成模塊的註入。
基於這樣一個約定就能很好的找到服務接口的實現類,而不需要在代碼裡制定。
同時,JDK官方也提供瞭一個查找服務實現者的工具類:java.util.ServiceLoader
線程上下文類加載器就是雙親委派模型的破壞者,可以在執行線程中打破雙親委派機制的加載鏈關系,從而使得程序可以逆向使用類加載器
1.線程上下文類加載器(Context Classloader)
線程上下文類加載器(Context Classloader)是從JDK1.2開始引入的,類Thread中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)分別用來獲取和設置上線文類加載器
如果沒有通過setContextClassLoader(ClassLoader cl)進行設置的話,線程將繼承其父線程的上下文類加載器。
Java應用運行時的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過該類加載器來加載類與資源
它可以打破雙親委托機制,父ClassLoader可以使用當前線程的Thread.currentThread().getContextClassLoader()所指定的classLoader來加載類,這就可以改變父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關系的ClassLoader加載的類的情況,即改變瞭雙親委托模型
對於SPI來說,有些接口是Java核心庫所提供的,而Java核心庫是由啟動類加載器加載的,而這些接口的實現卻是來自於不同jar包(廠商提供),Java的啟動類加載是不會加載其他來源的jar包,這樣傳統的雙親委托模型就無法滿足SPI的要求。而通過給當前線程設置上下文類加載器,就可以由設置的上線文類加載器來實現與接口實現類的加載
Java提供瞭很多核心接口的定義,這些接口被稱為SPI接口,同時為瞭方便加載第三方的實現類,SPI提供瞭一種動態的服務發現機制(約定),隻要第三方在編寫實現類時,在工程內新建一個META-INF/services/
目錄並在該目錄下創建一個與服務接口名稱同名的文件,那麼在程序啟動的時候,就會根據約定去找到所有符合規范的實現類,然後交給線程上下文類加載器進行加載處理
MySQL的Driver驅動類
在使用 JDBC 時,都需要加載 Driver 驅動,不寫
Class.forName("com.mysql.jdbc.Driver")
或
Class.forName("com.mysql.cj.jdbc.Driver")
也可以讓 com.mysql.jdbc.Driver 正確加載
在MySQL6.0之後的jar包中,遺棄瞭之前的com.mysql.jdbc.Driver驅動,而是使用com.mysql.cj.jdbc.Driver取而代之,因為後者不需要再自己通過Class.forName("com.mysql.jdbc.Driver")
這種方式手動註冊驅動,全部都可以交由給SPI機制處理
在使用 JDBC,MySQL的com.mysql.cj.jdbc.Driver
的驅動類,主要就是用瞭Java中SPI定義的一個核心類:DriverManager
,該類位於rt.jar
包中,是Java中用於管理不同數據庫廠商實現的驅動,同時這些各廠商實現的Driver
驅動類,都繼承自Java的核心類java.sql.Driver
DriverManager 的類加載器
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的類加載器是 Bootstrap ClassLoader,會到 JAVA_HOME/jre/lib 下搜索類,但 JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-xx.xx.xx.jar 包
public class DriverManager { // 註冊驅動的集合 private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); // 初始化驅動 static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } }
loadInitialDrivers() 方法
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() // 1、使用 ServiceLoader 機制加載驅動,即 SPI AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); // 2、使用 jdbc.drivers 定義的驅動名加載驅動 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); // 這裡的 ClassLoader.getSystemClassLoader() 就是應用程序類加載器 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
從DriverManager中的loadInitialDrivers我們可以得知,我們即使不使用Class.forName(“com.mysql.cj.jdbc.Driver”),mysql的驅動也能被加載,這是因為後期jdk使用瞭ServiceLoader
2 是使用 Class.forName 完成類的加載和初始化,關聯的是應用程序類加載器,因此 可以順利完成類加載
1 就是 Service Provider Interface (SPI) 約定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名為文件,文件內容是實現類名稱
2.ServiceLoader
ServiceLoader 是一個簡單的加載服務提供者的機制。通常服務提供者會實現服務當中所定義的接口。服務提供者可以以一種擴展的jar包的形式安裝到java平臺上擴展目錄中,也可以添加到應用的classpath中。
1、服務提供者需要提供一個無參數的構造方法
2、服務提供者是通過在META-INF/services目錄下相應的提供者配置文件,該配置文件的文件名由服務接口的包名組成。
3、提供者配置文件裡面就是實現這個服務接口的類路徑,每個服務提供者占一行。
4、ServiceLoader是按需加載和實例化提供者的,就是懶加載,ServiceLoader其中還包含一個服務提供者緩存,裡面存放著已經加載的服務提供者。
5、ServiceLoader會返回一個iterator迭代器,會返回所有已經加載瞭的服務提供者
6、ServiceLoader是線程不安全的
使用ServiceLoader
ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class); Iterator<接口類型> iter = allImpls.iterator(); while(iter.hasNext()) { iter.next(); }
例:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class); Iterator<Driver> iterator = loader.iterator(); while (iterator.hasNext()){ Driver dirver = iterator.next(); System.out.println(dirver.getClass()+", 類加載器:"+dirver.getClass().getClassLoader()); } System.out.println("當前線程上線文類加載器:"+Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader類加載器:"+loader.getClass().getClassLoader());
ServiceLoader.load 方法
public static <S> ServiceLoader<S> load(Class<S> service) { // 獲取線程上下文類加載器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
線程上下文類加載器是當前線程使用的類加載器,默認就是應用程序類加載器,它內部又是由 Class.forName 調用瞭線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內部類 LazyIterator 中
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
可參考
剖析Java類加載器
Java中線程上下文類加載器的講解
四、自定義類加載器
1、自定義類加載器場景
1、加載非 classpath 隨意路徑中的類文件
2、通過接口來使用實現,希望解耦時(常用在框架設計)
3、不同應用的同名類都可以加載,不沖突,常見於 tomcat 容器
2、自定義類加載器步驟
1、繼承 ClassLoader 父類
2、遵從雙親委派機制,重寫 findClass 方法
註:不是重寫 loadClass 方法,否則不會走雙親委派機制
3、讀取類文件的字節碼
4、調用父類的 defineClass 方法來加載類
5、使用者調用該類加載器的 loadClass 方法
public class Load7 { public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader(); Class<?> c1 = classLoader.loadClass("TestServiceImpl"); Class<?> c2 = classLoader.loadClass("TestServiceImpl"); System.out.println(c1 == c2);//true MyClassLoader classLoader2 = new MyClassLoader(); Class<?> c3 = classLoader2.loadClass("TestServiceImpl"); //雖然相同類名,但不是同一個類加載器加載的 System.out.println(c1 == c3);//false c1.newInstance(); } } class MyClassLoader extends ClassLoader { @Override // name 就是類名稱 protected Class<?> findClass(String name) throws ClassNotFoundException { String path = "D:\\myclasspath\\" + name + ".class"; try { ByteArrayOutputStream os = new ByteArrayOutputStream(); Files.copy(Paths.get(path), os); // 得到字節數組 byte[] bytes = os.toByteArray(); // byte[] -> *.class return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("類文件未找到", e); } } }
到此這篇關於Java類加載器與雙親委派機制和線程上下文類加載器專項解讀分析的文章就介紹到這瞭,更多相關Java類加載器內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 面試必時必問的JVM 類加載機制詳解
- java基礎–JDK SPI概述
- JVM分析之類加載機制詳解
- ClassLoader雙親委派模式作用詳解
- 使用springboot通過spi機制加載mysql驅動的過程