您好,登錄后才能下訂單哦!
本篇內容主要講解“何為雙親委派原則”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“何為雙親委派原則”吧!
我敢打賭大家在開發過程中經常碰到一些類加載的問題,比如:
ClassNotFoundException
Cause: java.lang.ClassNotFoundException: Cannot find class: com.cc.A
NoClassDefFoundError
Cause: java.lang.NoClassDefFoundError: Cannot find class: com.cc.A
上述問題均和java類加載有關,如果不清楚JVM中類加載的原理,上述問題會讓人郁悶至極,僥幸在網上找到解決方案也只是暫時解決問題,后續在另外的場景中碰到又會繼續懵逼。
我這篇文章將對 Java 類加載器的雙親委派加載原理進行闡述,并結合實例程序深究類的雙親委派加載機制,大家徹底了解掌握類加載原理,清楚了類加載原理后,碰到上述類似問題就能快速解決,并在后續開發中避免類似問題。
什么是Java類加載?
java類加載器負責將編譯好的 Java class 件加載到 Java 虛擬機(JVM)中的運行時數據區中,供執行引擎調用。
java類加載在JVM體系結構中的位置如圖所示:
jvm體系結構原圖
沒有類加載機制,編寫的java程序就沒法在JVM中運行,因此掌握java類加載是非常重要的。
JVM類加載層級關系
執行java程序時,會啟動一個JVM進程,JVM在啟動時會做一些初始化操作,比如獲取系統參數等等,然后創建一個啟動類加載器,用于加載JVM運行時必須的一些類到內存中,同時也會創建其他兩個類加載器擴展類加載器和系統類加載器。
啟動類加載器、擴展類加載器和系統類加載器之間的關系如下圖所示:
jvm內置classLoader
**啟動類加載器:**java虛擬機啟動后創建的第一個類加載器,由C++語言實現,所以我們在java代碼中查看其信息時,看到的均為null。
擴展類加載器:由啟動類加載器加載,并將擴展類加載器中的parent的值設置為null(表示指向啟動類加載器),同時繼承自URLClassLoader。
系統類加載器:由啟動類加載器加載,并將系統類加載期中的parent的值設置為上述創建的擴展類加載器。,同時繼承自URLClassLoader。
在代碼中可以通過如下方式查看類加載中的parent指向:
代碼查看類加載器的parent
注意:這里的parent不是java的繼承機制,而是類加載器中的一個實例屬性,用于在類加載時的委托對象,parent屬性定義在其所繼承的ClassLoader中,定義如下所示。
public abstract class ClassLoader { .................... // The parent class loader for delegation private final ClassLoader parent;
JVM類加載的默認加載路徑
每種類型的類加載器默認都會有自己的加載路徑,啟動類加載器、擴展類加載器和系統類加載器的默認加載路徑如下圖所示:
三種類加載器的加載路徑
如上圖所示:
1、啟動類加載器(BootClassLoader)由C++語言編寫,負責在JVM啟動時加載jdk自身的一些核心class類(jar包形式)到JVM中,加載時尋找資源的路徑由只讀系統屬性:”sun.boot.class.path“ 指定,一般為:”JAVA_HOME/jre/classes“目錄(在該目錄下只能放class文件,jar包形式文件不生效)。
查看啟動類加載類加載路徑可以通過獲取系統屬性:”sun.boot.class.path“進行查看,如圖所示:
lancher中設置啟動類加載路徑
啟動類加載器加載路徑
2、擴展類加載器(ExtClassLoader),負責加載位于系統屬性:"java.ext.dirs"指向的目錄下加載class文件(jar包或者直接class文件形式)到JVM中,比如通常ext類加載路徑為:”$JAVA_HOMEx/jre/lib/ext“ 。
支持在JVM啟動之前進行修改路徑,運行中修改路徑不生效,擴展類路徑中僅支持jar包的加載。
查看擴展類加載器的類加載路徑可以通過獲取系統屬性:”java.ext.dirs“進行查看或向上轉型為URLClassLoader(上面說擴展類加載器繼承自URLClassLoader),查看位于父類URLClassLoader中urls屬性的方式進行查看,如圖所示:
擴展類加載器路徑
3、系統類加載器(AppClassLoader),負責加載應用classpath路徑下的class文件(jar包或者直接class文件形式)到JVM中,當系統中沒有設置classpath路徑時,默認加載當前路徑下的class文件。
查看系統類加載器的類加載路徑可以通過獲取系統屬性:”java.class.path“進行查看或向上轉型為URLClassLoader上面說擴展類加載器繼承自URLClassLoader),查看位于父類URLClassLoader中urls屬性的方式進行查看,如圖所示:
系統類加載路徑
JVM類加載雙親委托機制
JVM加載class類文件到虛擬機時,默認首先采用系統類加載器去加載用到的class類,采用的是雙親委托加載機制。
所謂雙親委托,顧名思義,就是當前類加載器(以系統類加載器為例)在加載一個類時,委托給其雙親(注意這里的雙親指的是類加載器中parent屬性指向的類加載器)先進行加載。
雙親類加載器在加載時同樣委托給自己的雙親,如此反復,直到某個類加載器沒有雙親為止(通常情況下指雙親為null,也即為當前的雙親為擴展類加載器,其parent為啟動類加載器),然后開始在依次在各自的類路徑下尋找、加載class類。
如下圖所示:
雙親委派
雙親委托加載實例實例
采用JDK版本
java version "1.8.0_261" Java(TM) SE Runtime Environment (build 1.8.0_261-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)
本實例涉及到兩個類:TestMain.java 和 A.java,期中TestMain為啟動類,在啟動類中調用類A中的方法執行進行輸出,分別輸出啟動類和被依賴類的類加載器信息,類定義如下所示:
A_java
TestMain
我們將兩個java文件拷貝到某個目錄下,在我本地比如放在E:\java_app目錄下,windows下打開命令行窗口,切換到E:\java_app,對當前java文件進行編譯,執行命令javac TestMain.java。
此時會在當前目錄下生產對應的class文件(這里只需要對TestMain執行編譯命令,因為TestMain依賴了A,所以Jdk編譯器就會自動先去編譯依賴的A),如圖所示:
編譯命令
接下來我們將觀察java類加載機制是怎樣實現雙親委托加載的。
委托給擴展類加載器加載
由于擴展類在自身類路徑下加載只支持尋找jar包的方式,因此我們通過工具將A.class文件打包進A.jar。
然后將A.jar放置到擴展類加載路徑:$JAVA_HOME/jre/lib/ext,同時保留當前目錄中的A.class文件。如圖所示:
擴展委派
此時在當前目錄:E:\java_app下仍然保留有A.class文件,在擴展類加載器路徑下多了一個包含了A.class的A.jar文件,在當前目錄下執行java命令執行TestMain,命令為:java TestMain,輸出如下所示:
擴展委派結果
由上圖輸出結果可知,class A雖然在系統類加載器的加載路徑中,但由于類加載的委托機制,A首先將由系統類加載器委托給其雙親擴展類加載器進行加載,剛好在擴展類加載器的加載路徑中包含了A.class(包含在A.jar中),所以A最終由擴展類加載器進行了加載。
委托給啟動類加載器進行加載
通常情況下,普通類的加載不應該委托給啟動類加載器進行加載,因為前面說過啟動類加載器由C++實現,在java虛擬機啟動時生成的,在java環境中獲取她的信息均為null。
本實例為了探究類加載的雙親委托機制,所以特意將構造一個將普通類委托給其加載的場景。
前面在講到啟動類加載器加載路徑時指出了啟動類加載器的加載路徑由只讀系統屬性”sun.boot.class.path“ 指定,且僅支持加載該目錄下固定的jar文件。
在jdk8中還有”$JAVA_HOME/jre/classes“目錄也是啟動類加載器加載的路徑(該路徑默認可能不存在,可以手工創建一個),在該目錄下只能放class文件,jar包形式文件不生效。
因此,本實例程序將當前目錄下的A.class文件拷貝到啟動類加載器的類路徑:”$JAVA_HOME/jre/classes“中,同時保留當前目錄中的A.class文件,也保留擴展類加載器類路徑中的A.jar。
類存放路徑如圖所示:
委派啟動
在當前目錄:E:\java_app目錄下執行命令運行TestMain,命令為:java TestMain,輸出如下所示:
委派啟動結果
由上圖輸出結果可知,class A雖然在系統類加載器的加載路徑中,也存在擴展類加載器的加載路徑中,但由于類加載的委托機制,A首先將由系統類加載器委托給其雙親擴展類加載器進行加載。
擴展類加載器又會繼續進行委托加載(實際上因為擴展類加載器的parent:啟動類加載器為null,所以此時的委托動作實際上就是去啟動類加載器的加載路徑中尋找class A),最終由啟動類加載進行了A的加載。
雙親委托加載方向
類加載器在加載類時,只能向上遞歸委托其雙親進行類加載,而不可能從雙親再反向委派當前類加載器來進行類加載。
在中國象棋中,卒子過河之后的行走軌跡永遠只能是前進或者左右平移,可以很形象的比作雙親委托類加載的這種方向性。
卒子過河比喻當前類加載器委派其雙親加載了某個類。這個類的后續依賴的加載已經和當前類加載器沒有關系。
過河之后的卒子只能前進,表示雙親在加載類的依賴類時,只能繼續遞歸進行雙親委派。
左右平移表示雙親在遞歸雙親委派加載失敗后,在雙親類加載器自己的加載路徑中進行加載。
為了表明委派具有方向性,我們繼續拿上面的TestMain.class和A.class兩個類做實驗。
上述委托實例中我們的場景時是:TestMain中依賴了A,我們將A通過雙親委托方式進行了加載,本次實驗中,我們將TestMain委托給雙親加載。
參照上述的操作步驟,將TestMain.class打進TestMain.jar中,放到擴展類加載器的加載路徑中,同時也保留TestMain.class到當前目錄,如下圖所示:
委派加載順序1
切換到當前應用目錄,執行java命令運行程序:java TestMain,執行結果如下所示:
委派順序執行結果
如上圖所示,出現錯誤了,TestMain被擴展類加載器加載了,依賴的A卻沒有能被加載到。
原因就是上述說的委派加載具有方向性導致的:
1、運行java命令執行TestMain程序時,系統類加載器準備加載TestMain,根據雙親委派機制,先委派給其雙親進行加載,最后,雙親擴展類加載器在其加載路徑中的TestMain.jar中找到了TestMain.class,完成了TestMain的加載。
2、TestMain中依賴了A,此時,會根據加載了TestMain的類加載器:擴展類加載器去加載A,加載方式根據委托機制遞歸委托給雙親加載,擴展類加載器的雙親為啟動類加載器,在啟動類加載器的加載路徑中不存在A,加載失敗,此時由擴展類加載器在自己的加載路徑中加載A,也因為加載路徑中沒有A.class存在,A.class存在于系統類加載器的加載路徑中,但是擴展類加載器不會再返回去委托系統類加載器進行加載,所以直接拋出加載失敗異常,出現了上述的錯誤。
到此,相信大家對“何為雙親委派原則”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。