首先我們了解下什么是雙親委派機制?當某個類加載器需要加載某個.class
文件時,它首先把這個任務委托給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己才會去加載這個類。
Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象,而且,加載某個類的class文件時,Java虛擬機采用的是雙親委派機制,即把請求交由父類處理,它是一種任務委派模式。
其工作原理:
(1)如果一個類加載器收到了類加載請求,它並不會自己先加載,而是把這個請求委托給父類的加載器去執行
(2)如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的引導類加載器;
(3)如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成加載任務,子加載器才會嘗試自己去加載,這就是雙親委派機制
(4)父類加載器一層一層往下分配任務,如果子類加載器能加載,則加載此類,如果將加載任務分配至系統類加載器也無法加載此類,則拋出異常
一、知識准備:類加載器的類別
在介紹雙親委派機制的時候,不得不提ClassLoader(類加載器)。說ClassLoader之前,我們得先了解下Java的基本知識。
Java是運行在Java的虛擬機(JVM)中的,但是它是如何運行在JVM中了呢?我們在IDE中編寫的Java源代碼被編譯器編譯成.class的字節碼文件。然后由我們的ClassLoader負責將這些class文件給加載到JVM中去執行。
(1)我們定義的類,如果我們要在編碼中用到這個類,首先就是要先把“*.java”這個文件編譯成class文件,然后由對應的“類加載器”加載到JVM中,我們才能夠使用這個“類對象”。
(2)一般的場景下,類的加載是在我們程序啟動的時候由jvm來完成,但是有些場景可能需要我們手動去指定加載某個類或找到某個類,這時候就要用到 Class.forName(String className) 加載/找到 這個className對應的類。
(3)如果比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這個兩個類就必定不相等。
JVM中提供了三層的ClassLoader:
1、BootstrapClassLoader(啟動類加載器)
Bootstrap classLoader主要負責加載核心的類庫(java.lang.*等),構造ExtClassLoader和APPClassLoader。JDK自帶的一款類加載器,用於加載JDK內部的類。Bootstrap類加載器用於加載JDK中$JAVA_HOME/jre/lib下面的那些類,比如rt.jar包里面的類。
c++
編寫,加載java
核心庫 java.*
,構造ExtClassLoader
和AppClassLoader
。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。
2、ExtClassLoader (標准擴展類加載器)
ExtClassLoader主要負責加載 jre/lib/ext 目錄下的一些擴展的jar。主要用於加載JDK擴展包里的類。一般$JAVA_HOME/lib/ext下面的包都是通過這個類加載器加載的,這個包下面的類基本上是以javax開頭的。
java
編寫,加載擴展庫,如classpath
中的jre
,javax.*
或者java.ext.dir
指定位置中的類,開發者可以直接使用標准擴展類加載器。
3、AppClassLoader(系統類加載器)
AppClassLoader:主要負責加載應用程序的主函數類。用來加載開發人員自己平時寫的應用代碼的類的,加載存放在classpath路徑下的那些應用程序級別的類的。
java
編寫,加載程序所在的目錄,如user.dir
所在的位置的class
其實還有一種類加載器,就是下面第4種。
4、CustomClassLoader(用戶自定義類加載器)
java
編寫,用戶自定義的類加載器,可加載指定路徑的class
文件
5、為什么應該叫做“父委派模型”,而不是“雙親委派機制”
這是個很蛋疼的翻譯問題,實際上在oracle官方文檔上,人家是這樣描述的:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
java平台通過委派模型去加載類。每個類加載器都有一個父加載器。當需要加載類時,會優先委派當前所在的類的加載器的父加載器去加載這個類。如果父加載器無法加載到這個類時,再嘗試在當前所在的類的加載器中加載這個類。(所以,java的類加載機制應該叫做“父委派模型”,不應該叫做“雙親委派機制”,“雙親委派機制”這個名字太具有誤導性了。)
二、雙親委派機制
如果有一個我們寫的Hello.java編譯成的Hello.class文件,它是如何被加載到JVM中的呢?別着急,請繼續往下看。
1、源碼分析
打開AndroidStudio,搜索“ClassLoader”,然后打開“java.lang”包下的ClassLoader類,然后將代碼翻到loadClass方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先檢查這個classsh是否已經加載過了
Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // c==null表示沒有加載,如果有父類的加載器則讓父類加載器加載
if (parent != null) { c = parent.loadClass(name, false); } else { //如果父類的加載器為空 則說明遞歸到bootStrapClassloader了 //bootStrapClassloader比較特殊無法通過get獲取
c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} if (c == null) { //如果bootstrapClassLoader 仍然沒有加載過,則遞歸回來,嘗試自己去加載class
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; } }
其實這段代碼已經很好的解釋了雙親委派機制,下面這張圖來描述一下上面這段代碼的流程:
從上圖中我們就更容易理解了,當一個Hello.class這樣的文件要被加載時。
1、不考慮我們自定義類加載器,首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載了。如果沒有,那么會拿到父加載器,然后調用父加載器的loadClass方法。
2、父類中同理也會先檢查自己是否已經加載過,如果沒有再往上。
注意這個類似遞歸的過程,直到到達Bootstrap classLoader之前,都是在檢查是否加載過,並不會選擇自己去加載。
3、直到BootstrapClassLoader,已經沒有父加載器了,這時候開始考慮自己是否能加載了,如果自己無法加載,會下沉到子加載器去加載,一直到最底層。
4、如果沒有任何加載器能加載,就會拋出ClassNotFoundException。
5、“父委派模型”是怎么工作的?
舉個例子,當前有個Test.class,需要加載rt.jar中的java.lang.String,那么加載的流程如下圖所示,整體的加載流程是向上委托父加載器完成的。
如果整個鏈路中,父加載器都沒有加載這個類,且無法加載這個類時,才會由Test.class所在的加載器去加載某個類(例如希望加載開發人員自定義的類 Test2.class)。
三、為什么要設計這種機制
這種設計有個好處是,如果有人想替換系統級別的類:String.java,篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader加載過了(為什么?因為當一個類需要加載的時候,最先去嘗試加載的就是BootstrapClassLoader),所以其他類加載器並沒有機會再去加載,從一定程度上防止了危險代碼的植入。
“父委派模型”保證了系統級別的類的安全性,使一些基礎類不會受到開發人員“定制化”的破壞。
如果沒有使用父委派模型,而是由各個類加載器自行加載的話,如果開發人員自己編寫了一個稱為java.lang.String的類,並放在程序的ClassPath中,那系統將會出現多個不同的String類, Java類型體系中最基礎的行為就無法保證。應用程序也將會變得一片混亂。
雙親委派機制的作用:
1、防止重復加載同一個.class
。通過委托去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。
2、保證核心.class
不能被篡改。通過委托方式,不會去篡改核心.clas
,即使篡改也不會去加載,即使加載也不會是同一個.class
對象了。不同的加載器加載同一個.class
也不是同一個Class
對象。這樣保證了Class
執行安全。
有人總結了一張腦圖如下:
四、代碼示例
1、舉例 1 :我自己建立一個 java.lang.String 類,寫上 static 代碼塊
package java.lang; public class String { static{ System.out.println("我是自定義的String類的靜態代碼塊"); } }
在另外的程序中加載 String 類,看看加載的 String 類是 JDK 自帶的 String 類,還是我們自己編寫的 String 類
public class StringTest { public static void main(String[] args) { java.lang.String str = new java.lang.String(); System.out.println("hello,atguigu.com"); StringTest test = new StringTest(); System.out.println(test.getClass().getClassLoader()); } }
程序並沒有輸出我們靜態代碼塊中的內容,可見仍然加載的是 JDK 自帶的 String 類
為什么呢?由於我們定義的String類本應用系統類加載器,但它並不會自己先加載,而是把這個請求委托給父類的加載器去執行,到了擴展類加載器發現String類不歸自己管,再委托給父類加載器(引導類加載器),這時發現是java.lang包,這事就歸引導類加載器管,所以加載的是 JDK 自帶的 String 類
2、舉例 2 :
在我們自己的 String 類中整個 main() 方法
package java.lang; public class String { static{ System.out.println("我是自定義的String類的靜態代碼塊"); } //錯誤: 在類 java.lang.String 中找不到 main 方法
public static void main(String[] args) { System.out.println("hello,String"); } }
由於雙親委派機制找到的是 JDK 自帶的 String 類,但在引導類加載器的核心類庫API里的 String 類中並沒有 main() 方法
3、舉例 3:
在 java.lang 包下整個 ShkStart 類 (自定義類名)
package java.lang; public class ShkStart { public static void main(String[] args) { System.out.println("hello!"); } }
會報錯,出於保護機制,java.lang 包下不允許我們自定義類
通過上面的例子,我們可以知道,雙親機制可以
(1)避免類的重復加載
(2)保護程序安全,防止核心API被隨意篡改
- 自定義類:java.lang.String (沒用)
- 自定義類:java.lang.ShkStart(報錯:阻止創建 java.lang開頭的類)
參考文檔: