您好,登錄后才能下訂單哦!
這篇文章主要講解了“JVM雙親委派模型及SPI實現原理是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“JVM雙親委派模型及SPI實現原理是什么”吧!
我們知道類加載機制是將?個類從字節碼?件轉化為虛擬機可以直接使?類的過程,但是是誰來執?這個過程中的加載過程,它?是如何完成或者說保障了類加載的準確性和安全性呢?
答案就是類加載器以及雙親委派機制。
雙親委派模型的?作機制是:當類加載器接收到類加載的請求時,它不會??去嘗試加載這個類,?是把這個請求委派給?加載器去完成,只有當?類加載器反饋???法完成這個加載請求時,?加載器才會嘗試??去加載類。
我們可以從 JDK 源碼中將它的?作機制?窺究竟。
ClassLoader#loadClass(String, boolean)
這是在 jdk1.8 的 java.lang.ClassLoader 類中的源碼,這個?法就是?于加載指定的類。
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ public class ClassLoader { protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // ?先,檢查該類是否已經被當前類加載器加載,若當前類加載未加載過該類,調??類的加載類?法去加載該類(如果?類為null的話交給啟動類加載器加載) 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 foun // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in orde // 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; } } }
看完了上?的代碼,我們知道這就是雙親委派模型代碼層?的解釋:
當類加載器接收到類加載的請求時,?先檢查該類是否已經被當前類加載器加載;
若該類未被加載過,當前類加載器會將加載請求委托給?類加載器去完成;
若當前類加載器的?類加載器(或?類的?類……向上遞歸)為 null,會委托啟動類加載器完成加 載;
若?類加載器?法完成類的加載,當前類加載器才會去嘗試加載該類。
在 JVM 中預定義的類加載器有3種: 啟動類加載器 (Bootstrap ClassLoader)、擴展類加載器(ExtensionClassLoader)、 應?類/系統類加載器(App/SystemClassLoader),另外還有?種是 用戶自定義的類加載器 ,它們各?有各?的職責。
啟動類加載器作為所有類加載器的"?祖宗",是由C++實現的,不繼承于java.lang.ClassLoader 類。它在虛擬機啟動時會由虛擬機的?段C++代碼進?加載,所以它沒有?類加載器,在加載完成后,它會負責去加載擴展類加載器和應?類加載器。
啟動類加載器?于加載 Java 的核?類——位于 <JAVA_HOME>\lib 中,或者被 -Xbootclasspath 參數所指定的路徑中,并且是虛擬機能夠識別的類庫(僅按照?件名識別,如 rt.jar、tools.jar ,名字不符合的類庫即使放在lib?錄中也不會被加載)。
拓展類加載器繼承于 java.lang.ClassLoader 類,它的?類加載器是啟動類加載器,?啟動類加載器在 Java 中的顯示就是 null。
引? jdk1.8 ClassLoader#getParent() ?法的注釋,這個?法是?于獲取類加載器的?類加載器: Returns the parent class loader for delegation. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class loader's parent is the bootstrap class loader.
拓展類加載器負責加載 <JAVA_HOME>\lib\ext ?錄中的,或者被 java.ext.dirs 系統變量所指定的路徑的所有類。
需要注意的是擴展類加載器僅?持加載被打包為 .jar 格式的字節碼?件。
應?類加載器繼承于 java.lang.ClassLoader 類,它的?類加載器是擴展類加載器。
應?類加載器負責加載?戶類路徑 classpath 上所指定的類庫。
如果應?程序中沒有?定義的類加載器,?般情況下應?類加載器就是程序中默認的類加載器。
?定義類加載器繼承于 java.lang.ClassLoader 類,它的?類加載器是應?類加載器。
這是普某戶籍?定義的類加載器,可加載指定路徑的字節碼?件。
?定義類加載器需要繼承 java.lang.ClassLoader 類并重寫 findClass ?法(下?有說明為什么不重寫 loadClass ?法)?于實現?定義的加載類邏輯。
基于雙親委派模型規定的這種帶有優先級的層次性關系,虛擬機運?程序時就能夠避免類的重復加載。 當?類類加載器已經加載過類時,如果再有該類的加載請求傳遞到?類類加載器,?類類加載器執? loadClass ?法,然后委托給?類類加載器嘗試加載該類,但是?類類加載器執? Class<?> c = findLoadedClass(name);
檢查該類是否已經被加載過這?階段就會檢查到該類已經被加載過,直接返回該類,?不會再次加載此類。
雙親委派模型能夠避免核?類篡改。?般我們描述的核?類是 rt.jar、tools.jar 這些由啟動類加載器加載的類,這些類庫在?常開發中被?泛運?,如果被篡改,后果將不堪設想。 假設我們?定義了?個 java.lang.Integer 類,與好處1?樣的流程,當加載類的請求傳遞到啟動類加載器時,啟動類加載器執行findLoadedClass(String) ?法發現 java.lang.Integer 已經被加載過,然后直接返回該類,加載該類的請求結束。雖然避免核?類被篡改這?點的原因與避免類的重復加載?致,但這還是能夠作為雙親委派模型的好處之?的。
這?所說的不?也可以理解為打破雙親委派模型,當雙親委派模型不滿??戶需求時,?然是由于其不?之處也就促使?戶將其打破這?描述的也就是打破雙親委派模型的三種
由于歷史原因( ClassLoader 類在 JDK1.0 時就已經存在,?雙親委派模型是在 JDK1.2 之后才引?的),在未引?雙親委派模型時,?戶自定義的類加載器需要繼承 java.lang.ClassLoader 類并重寫 loadClass() 方法,因為虛擬機在加載類時會調? ClassLoader#loadClassInternal(String) ,而這個?法(源碼如下)會調?自定義類加載重寫的 loadClass() 方法。
而在引?雙親委派模型后, ClassLoader#loadClass ?法實際就是雙親委派模型的實現,如果重寫了此?法,相當于打破了雙親委派模型。為了讓?戶?定義的類加載器也遵從雙親委派模型, JDK新增了 findClass 方法,?于實現?定義的類加載邏輯。
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ public class ClassLoader { // This method is invoked by the virtual machine to load a class. private Class<?> loadClassInternal(String name) throws ClassNotFoundException { // For backward compatibility, explicitly lock on 'this' when // the current class loader is not parallel capable. if (parallelLockMap == null) { synchronized (this) { return loadClass(name); } } else { return loadClass(name); } } // 其余?法省略...... }
由于雙親委派模型規定的層次性關系,導致?類類加載器加載的類能訪問?類類加載器加載的類,??類類加載器加載的類?法訪問?類類加載器加載的類。為了讓上層類加載器加載的類能夠訪問下層類加載器加載的類,或者說讓?類類加載器委托?類類加載器完成加載請求,JDK 引?了線程上下?類加載器,藉由它來打破雙親委派模型的屏障。
當?戶需要程序的動態性,?如代碼熱替換、模塊熱部署等時,雙親委派模型就不再適?,類加載器會發展為更為復雜的?狀結構。
上?說到雙親委派模型的不?時提到了線程上下?類加載器 Thread Context ClassLoader ,線程上下?類加載器是定義在 Thread 類中的?個 ClassLoader 類型的私有成員變量,它指向了當前線程的類加載器。
上?已經提到線程上下?類加載能夠讓?類類加載器委托?類類加載器完成加載請求,那么這是如何實現的呢?下?就來討論?下。
我們知道 Java 提供了?些 SPI(Service Provider Interface) 接?,它允許服務商編寫具體的代碼邏輯來完成該接?的功能。
但是 Java 提供的 SPI 接?是在核?類庫中,由啟動類加載器加載的,?商實現的具體邏輯代碼是在classpath 中,是由應?類加載器加載的,?啟動類加載器加載的類?法訪問應?類加載器加載的類,也就是說啟動類加載器?法找到 SPI 實現類,單單依靠雙親委派模型就?法實現 SPI 的功能了,所以線程上下?類加載器應運??。
在 Java 提供的 SPI 中我們最常?的可能就屬 JDBC 了,下?我們就以 JDBC 為例來看?下線程上下?類加載器如何打破雙親委派模型。
回憶?下以前使? JDBC 的場景,我們需要創建驅動,然后創建連接,就像下?的代碼這樣:
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ public class ThreadContextClassLoaderDemoOfJdbc { public static void main(String[] args) throws Exception { // 加載 Driver 的實現類 Class.forName("com.mysql.jdbc.Driver"); // 建?連接 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234"); } }
在 JDK1.6 以后可以不?寫 Class.forName("com.mysql.jdbc.Driver"); ,代碼依舊能正常運?。這是因為?帶的 jdbc4.0 版本已?持 SPI 服務加載機制,只要服務商的實現類在classpath 路徑中,Java 程序會主動且?動去加載符合 SPI 規范的具體的驅動實現類,驅動的全限定類名在 META-INF.services ?件中。
所以,讓我們把?光聚焦于建?連接的語句,這?調?了 DriverManager 類的靜態?法getConnection 。
在調?此?法前,根據類加載機制的初始化時機,調?類的靜態?法會觸發類的初始化,當 DriverManager 類被初始化時,會執?它的靜態代碼塊。
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; // 省略代碼:?先讀取系統屬性 jdbc.drivers // 通過 SPI 加載 classpath 中的驅動類 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // ServiceLoader 類是 SPI 加載的?具類 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; } }); // 省略代碼:使?應?類加載器繼續加載系統屬性 jdbc.drivers 中的驅動類 } }
從上?的代碼中可以看到,程序時通過調? ServiceLoader.load(Driver.class) ?法來完成?動加載。classpath 路徑中具體的所有實現了 Driver.class 接?的?商實現類,?在 ServiceLoader.load() 方法中,就是獲取了當前線程上下?類加載器,并將它傳遞下去,將它作為類加載器去實現加載邏輯的。
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ public final class ServiceLoader<S> implements Iterable<S>{ public static <S> ServiceLoader<S> load(Class<S> service) { // 獲取當前線程的線程上下?類加載器 AppClassLoader,?于加載 classpath 中的具體實現類 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } }
JDK 默認加載當前類的類加載器去加載當前類所依賴且未被加載的類,而ServiceLoader 類位于java.util 包下,?然是由啟動類加載器完成加載,而廠商實現的具體驅動類是位于 classpath 下,啟動類加載器?法加載 classpath 目錄的類,而如果加載具體驅動類的類加載器變成了應?類加載器,那么就可以完成加載了。
通過跟蹤代碼,不難看出 ServiceLoader#load(Class) ?法創建了?個 LazyIterator 類同時返回了?個 ServiceLoader 對象,前者是?個懶加載的迭代器,同時它也是后者的?個成員變量,當對迭代器進行遍歷時,就觸發了?標接?實現類的加載。
/** * @author 庭前云落 * @Date 2020/8/22 21:29 * @description */ private class LazyIterator implements Iterator<S> { public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } }
在 DriverManager#loadInitialDrivers ?法,也就是 DriverManager 類的靜態代碼塊所執?的?法中,有這樣?段代碼:
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 對象,在這個對象中有?個 LazyIterator 迭代器類,?于存放所有?商實現的具體驅動類,當我們對 LazyIterator 這個迭代器進?遍歷時,就出發了類加載的邏輯。
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 不?寫 Class.forName("com.mysql.jdbc.Driver"); 的原因就是在此處會?動調?這個?法 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 }
每次遍歷都會調? Class.forName(cn, false, loader) ?法對指定的類進?加載和實例化操作,這也是前?提到的在 jdk1.6 以后不?在寫 Class.forName("com.mysql.jdbc.Driver"); 的原因。
在這個?法 Class.forName(cn, false, loader) 中,傳?的參數 cn 是全路徑類名,false 是指不進?初始化,loader 則是指定完成 cn 類加載的類加載器。
在這?的 loader 變量,我們回顧?下前?的描述,在 ServiceLoader.load(Driver.class) ?法中是不是獲取了線程上下?類加載器并傳遞下去?
不記得?在回過頭去看?遍!
?傳?的線程上下?類加載器會作為參數傳遞給 ServiceLoader 類的構造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null "); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); }
?此處的 cl 變量就是調? DriverManager 類靜態?法的線程上下?類加載器,即應?類加載器。
也就是說,通過 DriverManager 類的靜態?法,實現了由 ServiceLoader 類觸發加載位于 classpath的?商實現的驅動類。前?已經說過, ServiceLoader 類位于 java.util 包中,是由啟動類加載器加載的,?由啟動類加載器加載的類竟然實現了"委派"應?類加載器去加載驅動類,這?疑是與雙親委派機制相悖的。?實現這個功能的,就是線程上下?類加載器。
感謝各位的閱讀,以上就是“JVM雙親委派模型及SPI實現原理是什么”的內容了,經過本文的學習后,相信大家對JVM雙親委派模型及SPI實現原理是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。