Java類載入器算是一個老生常談的問題,大多Java工程師也都對其中的知識點倒背如流,最近在看源碼的時候發現有一些細節的地方理解還是比較模糊,正好寫一篇文章梳理一下。 關於Java類載入器的知識,網上一搜一大片,我自己也看過很多文檔,博客。資料雖然很多,但還是希望通過本文儘量寫出一些自己的理解,自己 ...
Java類載入器算是一個老生常談的問題,大多Java工程師也都對其中的知識點倒背如流,最近在看源碼的時候發現有一些細節的地方理解還是比較模糊,正好寫一篇文章梳理一下。
關於Java類載入器的知識,網上一搜一大片,我自己也看過很多文檔,博客。資料雖然很多,但還是希望通過本文儘量寫出一些自己的理解,自己的東西。如果只是重覆別人寫的內容那就失去寫作的意義了。
類載入器結構
名稱解釋:
- 根類載入器,也叫引導類載入器、啟動類載入器。由於它不屬於Java類庫,這裡就不說它對應的類名了,很多人喜歡稱BootstrapClassLoader。本文都稱之為根類載入器。
載入路徑:<JAVA_HOME>\lib - 擴展類載入器,對應Java類名為ExtClassLoader,該類是sun.misc.Launcher的一個內部類。
載入路徑:<JAVA_HOME>\lib\ext - 應用類載入器,對應Java類名為AppClassLoader,該類是sun.misc.Launcher的一個內部類。
載入路徑:用戶目錄
//可以通過這種方式列印載入路徑 System.out.println("boot:"+System.getProperty("sun.boot.class.path")); System.out.println("ext:"+System.getProperty("java.ext.dirs")); System.out.println("app:"+System.getProperty("java.class.path"));
重點說明:
- 根類載入器對於普通Java工程師來講可以理解成一個概念上的東西,因為我們無法通過Java代碼獲取到根類載入器,它屬於JVM層面。
- 除了根類載入器之外,其他兩個擴展類載入器和應用類載入器都是通過類sun.misc.Launcher進行初始化,而Launcher類則由根類載入器進行載入。
看下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); //刪除了一些安全方面的代碼 //... }
雙親委派模型
雙親委派模型是指當我們調用類載入器的loadClass方法進行類載入時,該類載入器會首先請求它的父類載入器進行載入,依次遞歸。如果所有父類載入器都載入失敗,則當前類載入器自己進行載入操作。
邏輯很簡單,通過ClassLoader類的源碼來分析一下。
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) { //如果當前類沒有被載入且父類載入器不為null,則請求父類載入器進行載入操作 c = parent.loadClass(name, false); } else { //如果當前類沒有被載入且父類載入器為null,則請求根類載入器進行載入操作 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); //如果父類載入器載入失敗,則由當前類載入器進行載入, c = findClass(name); //進行一些統計操作 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //初始化該類 if (resolve) { resolveClass(c); } return c; } }
雙親委派模型的實現邏輯總體看還是非常簡單明瞭的。
這裡有幾個細節需要說明:
- ClassLoader類是一個抽象類,但卻沒有包含任何抽象方法。
- 如果要實現自己的類載入器且不破壞雙親委派模型,只需要繼承ClassLoader類並重寫findClass方法。
- 如果要實現自己的類載入器且破壞雙親委派模型,則需要繼承ClassLoader類並重寫loadClass,findClass方法。
令人疑惑的系統類載入器
當你把上面的知識都搞清楚以後,會發現ClassLoader類中有個方法是getSystemClassLoader,系統類載入器,這又是什麼?
系統類載入器是個容易讓人混淆的概念,我一度以為它就是應用類載入器的別名,就跟啟動類載入器和根類載入器道理一樣。事實上,預設情況下我們通過ClassLoader.getSystemClassLoader()獲取到的系統類載入器也確實是應用類載入器。
很多資料在說類載入器結構的時候會直接把應用類載入器說成是系統類載入器,其實我們通過類名就可以判斷兩個不是一回事。
系統類載入器可以通過System.setProperty("java.system.class.loader", xxx類名)進行自定義設置。
系統類載入器不是一個全新的載入器,它只是一個概念,本質上還是上述說的四大類載入器(把用戶自定義類載入器算進去),至於提出這個概念的原因以及使用場景,還需要繼續考究。
被人忽略的上下文類載入器
上面討論了各個類載入器的載入路徑。鑒於雙親委派模型的設計,子類載入器都保留了父類載入器的引用,也就是說當由子類載入器載入的類需要訪問由父類載入器載入的類時,毫無疑問是可以訪問到的。但考慮一種場景,會不會有父類載入器載入的類需要訪問子類載入器載入的類這種情況?如果有,怎麼解決(父類載入器並沒有子類載入器的引用)?
這就是我們要討論的常常被人們忽略的上下文類載入器。
經典案例:
JDBC是Java制定的一套訪問資料庫的標準介面,它包含在Java基礎類庫中,也就是說它是由根類載入器載入的。與此同時,各個資料庫廠商會各自實現這套介面來讓Java工程師可以訪問自己的資料庫,而這部分實現類庫是需要Java工程師在工程中作為一個第三方依賴引入使用的,也就是說這部分實現類庫是由應用類載入器進行載入的。
先上一段Java獲取Mysql連接的代碼:
//載入驅動程式 Class.forName("com.mysql.jdbc.Driver"); //連接資料庫 Connection conn = DriverManager.getConnection(url, user, password);
這裡DriverManager類就屬於Java基礎類庫,由根類載入器載入。我們可以通過它獲取到資料庫的連接,顯然是它通過com.mysql.jdbc.Driver驅動成功連接到了資料庫,上面也說了資料庫驅動(作為第三方類庫引入)是由應用類載入器載入的。這個場景就是典型的由父類載入器載入的類需要訪問由子類載入器載入的類。
Java是怎麼實現這種逆向訪問的呢?直接看DriverManager類的源碼:
//建立資料庫連接各個不同參數的方法最終都會走到這裡 private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { //獲取調用者的類載入器 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { //如果為null,則使用上下文類載入器 //這裡是重點,什麼時候類載入器才會為null? 當然就是由根類載入器載入的類了 if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } //...省略 for(DriverInfo aDriver : registeredDrivers) { //使用上下文類載入器去載入驅動 if(isDriverAllowed(aDriver.driver, callerCL)) { try { //如果載入成功,則進行連接 Connection con = aDriver.driver.connect(url, info); //... } catch (SQLException ex) { if (reason == null) { reason = ex; } } } //... } }
重點說明:
為什麼上下文類載入器就可以載入到資料庫驅動呢?回到上面一開始Launcher初始化類載入器的源碼,我們發現原來所謂的上下文類載入器本質上就是應用類載入器,有沒有豁然開朗的感覺?上下文類載入器只是為瞭解決類的逆向訪問提出來的一個概念,並不是一個全新的類載入器,它本質上就是應用類載入器。
基本上我理解的Java類載入器就這麼多知識,如果有沒提到的或者是錯誤的地方,歡迎交流。
Java學習交流QQ群:523047986 禁止閑聊,非喜勿進!