1. 雙親委派模型
關於雙親委派模型,網上的資料有很多。我這裏只簡單的描述一下,就當是複習。
1.1 什麼是雙親委派模型?
首先,先要知道什麼是類加載器。簡單說,類加載器就是根據指定全限定名稱將class
文件加載到JVM
內存,轉爲Class
對象。如果站在JVM
的角度來看,只存在兩種類加載器:
啓動類加載器(
Bootstrap ClassLoader
):由C++
語言實現(針對HotSpot
),負責將存放在<JAVA_HOME>\lib
目錄或-Xbootclasspath
參數指定的路徑中的類庫加載到內存中。其他類加載器:由
Java
語言實現,繼承自抽象類ClassLoader
。如:
- 擴展類加載器(
Extension ClassLoader
):負責加載<JAVA_HOME>\lib\ext
目錄或java.ext.dirs
系統變量指定的路徑中的所有類庫。- 應用程序類加載器(
Application ClassLoader
)。負責加載用戶類路徑(classpath
)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。
雙親委派模型工作過程是:如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每個類加載器都是如此,只有當父加載器在自己的搜索範圍內找不到指定的類時(即ClassNotFoundException
),子加載器纔會嘗試自己去加載。雙親委派模型如下:
雙親委派模型過程
某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
使用雙親委派模型的好處在於Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程序將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行。
雙親委派模型的系統實現
在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父加載失敗,則拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
//check the class has been loaded or not
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//if throws the exception ,the father can not complete the load
}
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
從上面代碼可以明顯看出,loadClass(String, boolean)
函數即實現了雙親委派模型!整個大致過程如下:
- 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接返回。
- 如果此類沒有加載過,那麼,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用
parent.loadClass(name, false);
).或者是調用bootstrap
類加載器來加載。- 如果父加載器及
bootstrap
類加載器都沒有找到指定的類,那麼調用當前類加載器的findClass
方法來完成類加載。
話句話說,如果自定義類加載器,就必須重寫findClass
方法!
findclass方法
findClass
的默認實現如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看出,抽象類ClassLoader
的findClass
函數默認是拋出異常的。而前面我們知道,loadClass
在父加載器無法加載類的時候,就會調用我們自定義的類加載器中的findeClass
函數,因此我們必須要在loadClass
這個函數裏面實現將一個指定類名稱轉換爲Class
對象.
如果是是讀取一個指定的名稱的類爲字節數組的話,這很好辦。但是如何將字節數組轉爲Class
對象呢?很簡單,Java
提供了defineClass
方法,通過這個方法,就可以把一個字節數組轉爲Class對象啦~
defineClass方法
defineClass
主要的功能是:
將一個字節數組轉爲
Class
對象,這個字節數組是class
文件讀取後最終的字節數組。如,假設class
文件是加密過的,則需要解密後作爲形參傳入defineClass
函數。
defineClass
默認實現如下:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}
我們看一下整個函數的調用過程:
最後
整理的一點思考和麪試題:
-
Java虛擬機的第一個類加載器是Bootstrap,這個加載器很特殊,它不是Java類,因此它不需要被別人加載,它嵌套在Java虛擬機內核裏面,也就是JVM啓動的時候Bootstrap就已經啓動,它是用C++寫的二進制代碼(不是字節碼),它可以去加載別的類。
這也是我們在測試時爲什麼發現
System.class.getClassLoader()
結果爲null的原因,這並不表示System這個類沒有類加載器,而是它的加載器比較特殊,是BootstrapClassLoader
,由於它不是Java類,因此獲得它的引用肯定返回null。 -
委託機制具體含義
當Java虛擬機要加載一個類時,到底派出哪個類加載器去加載呢?- 首先當前線程的類加載器去加載線程中的第一個類(假設爲類A)。
注:當前線程的類加載器可以通過Thread類的getContextClassLoader()獲得,也可以通過setContextClassLoader()自己設置類加載器。 - 如果類A中引用了類B,Java虛擬機將使用加載類A的類加載器去加載類B。
- 還可以直接調用
ClassLoader.loadClass()
方法來指定某個類加載器去加載某個類。
- 首先當前線程的類加載器去加載線程中的第一個類(假設爲類A)。
-
委託機制的意義 — 防止內存中出現多份同樣的字節碼
比如兩個類A和類B都要加載System類:- 如果不用委託而是自己加載自己的,那麼類A就會加載一份System字節碼,然後類B又會加載一份System字節碼,這樣內存中就出現了兩份System字節碼。
- 如果使用委託機制,會遞歸的向父類查找,也就是首選用Bootstrap嘗試加載,如果找不到再向下。這裏的System就能在Bootstrap中找到然後加載,如果此時類B也要加載System,也從Bootstrap開始,此時Bootstrap發現已經加載過了System那麼直接返回內存中的System即可而不需要重新加載,這樣內存中就只有一份System的字節碼了。
問:能不能自己寫個類叫java.lang.System
?
答案:通常不可以,但可以採取另類方法達到這個需求。
解釋:爲了不讓我們寫System類,類加載採用委託機制,這樣可以保證爸爸們優先,爸爸們能找到的類,兒子就沒有機會加 載。而System類是Bootstrap加載器加載的,就算自己重寫,也總是使用Java系統提供的System,自己寫的System類根本沒有機會得到加載。
但是,我們可以自己定義一個類加載器來達到這個目的,爲了避免雙親委託機制,這個類加載器也必須是特殊的。由於系統自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄,那麼系統的加載器就無法加載,也就是最終還是由我們自己的加載器加載。