java类加载和双亲委派模型浅说
本文目錄
- 前言
- 一、類加載器
- 1.1 類加載機制的基本特征
- 1.2 類加載的分類
- 1.3 類加載器
- A、啟動類加載器(引導類加載器,Bootstrap ClassLoader)
- B、擴展類加載器(Extension ClassLoader)
- C、應用程序類加載器(系統(tǒng)類加載器,AppClassLoaer)
- D、自定義類加載器
- 1.4 類加載器的必要性
- 二、雙親委派模型
- 2.1 概述
- 2.2 雙親委派模型的實現(xiàn)
- 2.3 雙親委派模型優(yōu)劣勢
- 2.3.1 優(yōu)勢
- 2.3.2 劣勢
- 2.4 破壞雙親委派模型
- 2.4.1 第一次破壞雙親委派模型
- 2.4.2 第二次破壞雙親委派模型
- 2.4.3 第三次破壞雙親委派模型
- 三、自定義類加載器
- 3.1 為啥要自定義類加載器?
- 3.2 常見的場景
- 3.3 實現(xiàn)方式
- 3.3.1 實現(xiàn)方式
- 3.3.2 兩種實現(xiàn)方式的對比
- 3.3.3 說明
前言
之前被問到雙親委派模型和如果沒有此模型會導致什么問題,我去,一下子懵了,但下來細想,這不就是問的此模型的優(yōu)勢嘛,我竟然沒有答出來,應該是我緊張了,自以為把此模型掌握的很扎實,實則不然,所以再來整理一下相關的知識點。
一、類加載器
說到雙親委派模型,不得不提一下類加載器,再說類加載器之前不得不說類加載的分類和類加載機制。
1.1 類加載機制的基本特征
類加載機制通常有三個基本特征:
A、雙親委派模型:首先說明并不是所有類加載都遵循這個模型。有的時候,啟動類加載器所加載的類型,是可能要加載用戶代碼的,比如JDK內(nèi)部的SPI機制,用戶可以在標準API框架上提供自己的實現(xiàn),JDK也需要提供些默認的參考實現(xiàn)。比如,java中JNDI,JDBC,文件系統(tǒng)等很多方面,都是利用的這種機制,這種情況就不會用雙親委派模型去加載,而是利用所謂的上下文加載器。
B、可見性:子類加載器可以訪問父類加載器加載的類型,凡是反過來是不允許的。不然,因為缺少必要的隔離,我們就沒有辦法利用類加載器去實現(xiàn)容器的邏輯。
C、單一性:由于父加載器的類型對于子加載器是可見的,所以父加載器中加載過的類型,就不會在子加載器中重復加載。但是注意,類加載器“鄰居”間,同一類型仍然可以被加載多次,因為互相并不可見。
1.2 類加載的分類
類的加載分為:顯式加載 VS 隱式加載
class文件的顯式加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式。
? 顯式加載指的是在代碼中通過調(diào)用ClassLoader加載class對象,如直接使用Class.forName("java.lang.String")或this.getClass().getClassLoader().loadClass()加載class對象。
? 隱式加載是通過虛擬機自動加載到內(nèi)存中,而不是直接在代碼中調(diào)用ClassLoader的方法加載class對象,如在加載某個類的class文件時,該類的class文件中引用了另一個類的對象,此時額外引用的類將通過JVM自動加載到內(nèi)存中。
在日常開發(fā)以上兩種方式一般會混合使用。
代碼示例一
package com.fanhf.javastudy.classloader;/** * @author fanhf * @Description 顯式和隱式加載 * @date 2021-02-07 14:22 */ public class UserTest{User user = new User();//隱式加載public static void main(String[] args){try{//顯式加載方式1Class clazz = Class.forName("com.fanhf.javastudy.classloader.User");//顯式加載方式2Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.fanhf.javastudy.classloader.User");}catch(ClassNotFoundException e){e.printStackTrace();}} } // User類 class User{static {System.out.println("我是User類的初始化");} }1.3 類加載器
JVM支持兩種類型的類加載器,分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
從概念上來講,自定義類加載器一般指的是程序中由開發(fā)人員自定義的一類類加載器,但是java虛擬機規(guī)范卻沒有這么定義,而是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器。無論類加載器的類型如何劃分,在程序中我們最常見的類加載器結構如下情況:
除了頂層的啟動類加載器外,其余的類加載器都應當有自己的“父類”加載器。
不同類加載器看似是繼承(Inheritance)關系,實際上是包含關系。在下層加載器中,包含著上層加載器的引用
A、啟動類加載器(引導類加載器,Bootstrap ClassLoader)
? 這個類加載使用C/C++語言實現(xiàn)的,嵌套在JVM內(nèi)部 ? 它用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內(nèi)容)。用于提供JVM自身需要的類。 ? 并不繼承自java.lang.ClassLoader,沒有父加載器。 ? 出于安全考慮,Bootstrap啟動類加載器只加載包名為:java、javax、sun等開頭的類 ? 加載擴展類和應用程序類加載器,并指定為他們的父類加載器。使用-XX:+TraceClassLoading參數(shù)得到。
在上面的代碼示例一中,加入-XX:+TraceClassLoading的啟動參數(shù),可以看到
此處省略許多類…
此處省略許多類…
在代碼示例一中,加載了504個類,包括以下包的類
java.lang、java.util、java.io、java.nio、java.net、java.security、
sun.misc、sun.reflect、sun.nio、sun.usagetracker、sun.launcher
B、擴展類加載器(Extension ClassLoader)
? java語言編寫,由sun.misc.Launcher$ExtClassLoader實現(xiàn)。 ? 繼承于ClassLoader類 ? 父類加載器為啟動類加載器從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄下加載類庫。如果用戶創(chuàng)建的JAR放在此目錄下,也會自動由擴展類加載器加載。
類加載器繼承關系如下圖:
ClassLoader的孩子們:
Launcher類中的實現(xiàn)
C、應用程序類加載器(系統(tǒng)類加載器,AppClassLoaer)
? java語言編寫,由sun.misc.Launcher$AppClassLoader實現(xiàn) ? 繼承于ClassLoader類 ? 父類加載器為擴展類加載器 ? 它負責加載環(huán)境變量classpath或系統(tǒng)屬性 java.class.path指定路徑下的類庫 ? 應用程序中的類加載器默認是系統(tǒng)類加載器 ? 他是用戶自定義類加載器的默認父加載器通過ClassLoader的getSystemClassLoader() 方法可以獲取到該類加載器
D、自定義類加載器
自定義類加載器可以實現(xiàn)應用隔離,比如Tomcat,Spring等中間件和組件框架都在內(nèi)部實現(xiàn)了自定義的加載器。自定義類加載器通過需要繼承自ClassLoader,此類中的loadClass方法中的邏輯就是雙親委派模型的實現(xiàn),繼承ClassLoader后,在jdk1.2之后,不建議去覆蓋loadClass方法,而是在findClass方法中實現(xiàn),findClass就是在loadClass中調(diào)用的,當loadClass方法中父類加載器加載失敗,就會調(diào)用自己寫的findClass方法來完成類的加載,這樣就可以保證自定義的類加載器也符合雙親委派模型。
通過看源碼得知,ClassLoader是一個抽象類,很多方法是空的沒有實現(xiàn),比如findClass()、findResource()等。而URLClassLoader這個實現(xiàn)為這些方法提供了具體的實現(xiàn)。并新增了URLClassPath類協(xié)助取得Class字節(jié)碼流等功能。在編寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔。
1.4 類加載器的必要性
一般情況下,java開發(fā)人員并不需要在程序中顯式的使用類加載器,但是了解類加載器的加載機制卻很重要。從以下幾點說明:
? 避免在開發(fā)中遇到java.lang.ClassNotFoundException異常或java.lang.NoClassDefFoundError異常時手足無措。只有了解類加載器的加載機制才能夠在出現(xiàn)異常的時候快速的根據(jù)錯誤異常日志定位問題。
? 需要支持類的動態(tài)加載或需要對編譯后的字節(jié)碼文件進行加解密操作時,就需要和類加載器打交道了。
開發(fā)人員可以在程序中編寫自定義類加載器來重新定義類的加載規(guī)則,以便實現(xiàn)一些自定義的處理邏輯。
贈送面試題:Class.forName()與ClassLoader.loadClass()
? Class.forName():是一個Class類中在靜態(tài)方法,屬于顯式加載,最常用的是Class.forName(String className);
根據(jù)傳入的類的全限定名返回一個Class對象。該方法在將Class文件加載到內(nèi)存的同時,會執(zhí)行類的初始化。如:Class.forName(“com.fanhf.javastudy.classloader.ClassLoaderTest”);
? ClassLoader.loadClass():這是一個實例方法,屬于隱式加載,需要一個ClassLoader對象來調(diào)用此方法。該方法將Class文件加載到內(nèi)存時,并不會執(zhí)行類的初始化,直到這個類第一次使用時才進行初始化。該方法因為需要得到一個ClassLoader對象,所以可以根據(jù)需要指定使用哪個類加載器,如:ClassLoader cl=……;
cl.loadClass(“com.fanhf.javastudy.classloader.ClassLoaderTest”)
二、雙親委派模型
簡單說完了類加載器和類加載機制,步入正題說一下雙親委派模型
2.1 概述
類加載器用來把類加載到java虛擬機中。從JDK1.2開始,類的加載過程采取雙親委派機制,這種機制能更好的保證java平臺的安全。如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載器任務,就成功返回。只有父類加載器無法完成此加載任務時,才自己去加載。
如下圖:
然而,java虛擬機規(guī)范中并沒有明確要求類加載器的加載機制一定要使用雙親委派模型,只是建議使用這種方式。在tomcat中,缺省的類加載器接到一個類加載任務,會自行加載,加載失敗才會委托給它的超類進行加載,這也是Servelet規(guī)范推薦的一種做法。
2.2 雙親委派模型的實現(xiàn)
雙親委派機制在java.lang.ClassLoader.loadClass(String,boolean)接口中體現(xiàn)。
重點分析一下loadClass方法
實現(xiàn)邏輯:
1)先在當前類加載器緩存中查找有無目標類,如果不為空,則調(diào)用parent.loadClass(name,false)接口進行加載。
2)判斷當前加載器的父類加載器是否為空,如果不為空,則調(diào)用parent.loadClass(name,false)接口進行加載。
3)反之,如果當前加載器的父類加載器為空,則調(diào)用findBootstrapClassOrNull(name)接口,讓引導類加載器進行加載。
4)如果通過以上3條路徑都沒有加載成功,則調(diào)用findClass(name)進行加載。該接口最終調(diào)用java.lang.ClassLoader 接口的defineClass系列的native接口加載目標java類。
舉個🌰:
有一個java.lang.Object類需要加載,這個類屬于java中核心的不能再核心的類了,因此一定只能由啟動類加載器(Bootstrap ClassLoader)進行加載。jvm按照上面的4步進行加載,首先從應用類加載器(AppClassLoader)的緩存中查找該類,但此時并無此類,所以由應用類加載器的父類加載器即擴展類加載器進行加載,于是擴展類加載器繼續(xù)從第一步開始重復。由于擴展類加載器的緩存中也一定找不到該類,于是進入第二步,擴展類的父加載器時null,因此調(diào)用findClass方法進行加載,最終通過啟動類加載器進行加載。
2.3 雙親委派模型優(yōu)劣勢
2.3.1 優(yōu)勢
A、避免類的重復加載,確保一個類的全局唯一性
java類隨著它的類加載器一起舉杯了一種帶有優(yōu)先級的層次關系,通過這種層級關閉可以避免類被重復加載,當父類已經(jīng)加載了該類時,就沒有必要下一級加載器在加載一次了。
B、保護程序的安全,防止核心API被隨意篡改。
2.3.2 劣勢
檢查類是否加載的委托過程是單向的。
這個加載方式雖然從結構上說比較清晰,使各個ClassLoader的職責非常明確,但是同時帶來一個問題,即頂層的ClassLoader無法訪問底層的ClassLoader所加載的類。
通常情況下,啟動類加載器的類為系統(tǒng)核心類,包括一些重復的系統(tǒng)接口,而在應用類加載器中,為應用類,按照這種模式,應用類訪問啟動類自然是沒有問題的,但是啟動類訪問應用類就會出現(xiàn)問題。比如在啟動類提供了一個接口,該接口需要在應用類中得以實現(xiàn),該接口還綁定了一個工廠方法,用于創(chuàng)建該接口的實力,而接口和工廠方法都在啟動類。
2.4 破壞雙親委派模型
雙親委派模型并不是一個具有強制性約束的模型,而是java設計者推薦給開發(fā)者們的類加載器實現(xiàn)方式。
在java的世界中大部分的類加載器都遵循這個模型,直到java模塊化出現(xiàn)為止,雙親委派模型主要出現(xiàn)過3次較大規(guī)模“被破壞”的情況。
2.4.1 第一次破壞雙親委派模型
第一次被破壞是發(fā)生在雙親委派模型出現(xiàn)之前的JDK1.2版本以前的古老的時代。
為了兼容1.2之前已有的類加載器的概念和抽象類java.lang.ClassLoader中可能已經(jīng)被用戶自定義來加載器的代碼,在1.2之后在此類中增加了一個protected的findClass()方法,引導用戶編寫的類加載邏輯時盡可能去重新這個方法,而不是在loadClass中編寫代碼。
上面我們分析過loadClass方法,按照此方法的邏輯,如果父類加載失敗,會自動調(diào)用findClass方法來完成加載,這樣不僅保證用戶可以按照自己的需求去加載,還可以保證新實現(xiàn)的類加載器是符合雙親委派模型的。
2.4.2 第二次破壞雙親委派模型
第二次被破壞是由于這個模型自身的缺陷所導致的,在2.3.2節(jié)提到此模型的劣勢是頂層類加載器無法訪問底層所加載的類,換言之,頂層類加載器加載的都是比較基礎的類,當基礎的類想要訪問應用類加載的類的方法時,這條路似乎被堵死了,實際上這種問題被一個叫做線程上下文類加載器給解決了。
線程上下文類加載器(Thread Context ClassLoader)可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創(chuàng)建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局內(nèi)部沒有設置過的話,這個類加載器就默認使用應用類加載器進行加載。
舉個🌰:JNDI(Java Name and Directory Interface)java命名和目錄接口,作用是為JAVA應用程序提供命名和目錄訪問服務的API,現(xiàn)在是java標準的服務,其代碼由啟動類加載器進行加載,這個接口需要調(diào)用其他廠商實現(xiàn)并部署在classPath下的服務提供者接口(Service Provider Interface,SPI)的代碼,那么就可以使用線程上下文類加載器去加載所需的SPI服務代碼,這就是典型的父類加載去請求子類加載器完成類加載的行為,這種行為實際上打通了雙親委派模型的層次結構來逆向使用類加載器,違背了雙親委派模型的一般性原則,java中涉及SPI的加載基本上都采用這種方式來完成。
2.4.3 第三次破壞雙親委派模型
第三次是由于用戶對程序動態(tài)性的追求而導致的,如代碼熱替換,模塊熱部署等。這里提一下IBM主導的OSGI實現(xiàn)模塊化熱部署,實現(xiàn)的關鍵是它自定義的類加載機制的實現(xiàn),每一個程序模塊(OSGI中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現(xiàn)代碼的熱替換,在OSGI環(huán)境下,類加載器不再是雙親委派模型推薦的樹狀結構,而是更為復雜的網(wǎng)狀結構。
(OSGI這玩意我接觸的也不多,至于類加載的實現(xiàn)方式我就不多說了,感興趣的小可愛可以自行學習。)
三、自定義類加載器
在1.3節(jié)中說到有四種類加載器,最后一種是自定義類加載器,那就再來說說這玩意!
3.1 為啥要自定義類加載器?
A、 隔離加載類
在某些框架內(nèi)進行中間件與應用的模塊隔離,把類加載到不同的環(huán)境。比如,阿里內(nèi)某容器框架通過自定義類加載器確保應用中依賴的jar包不會影響到中間件運行時使用的jar包。再比如:tomcat這類web應用服務器,內(nèi)部自定義了好幾種類加載器,用于隔離同一個web應用服務器上的不同應用程序。
B、 修改類加載的方式
類的加載模型并非強制,除bootstrap外,其他的加載并非不一定要引入,或者根據(jù)實際情況在某個時間點進行按需進行動態(tài)加載。
C、擴展加載源
比如從數(shù)據(jù)庫、網(wǎng)絡、甚至是電視機機頂盒進行加載
D、 防止源碼泄漏
java代碼容易被編譯和篡改,可以進行編譯加密。那么類加載也需要自定義,還原加密的字節(jié)碼。
3.2 常見的場景
1)實現(xiàn)類似進程內(nèi)隔離,類加載器實際上用作不同的命名空間,以提供類似容器,模塊化的效果。例如,兩個模塊依賴于某個類庫的不同版本,如果分別被不同的容器加載,就可以互不干擾。這個方面的集大成者是java EE和OSGI,JPMS等框架。
2)應用需要從不同的數(shù)據(jù)源獲取類定義信息,例如網(wǎng)絡數(shù)據(jù)源,而不是本地文件系統(tǒng)。或者是需要自己操縱字節(jié)碼動態(tài)修改或者生成類型。
3.3 實現(xiàn)方式
用戶通過定制自己的類加載器,這樣可以重新定義類的加載規(guī)則,以便實現(xiàn)一些自定義的處理邏輯。
3.3.1 實現(xiàn)方式
? java提供了抽象類,所有用戶自定義的類加載器都應該繼承ClassLoder類。 ? 在自定義ClassLoader的子類的時候,我們常見的會有2種做法。○ 方式一:重寫loadClass()方法○ 方式二:重寫findClass()方法--推薦3.3.2 兩種實現(xiàn)方式的對比
這兩種方式本質(zhì)差不多,畢竟 loadClass()也會調(diào)用findClass(),但是從邏輯上講我們最好不要直接修改loadClass()內(nèi)部邏輯。建議的做法是只在findClass()里重寫自定義類的加載方法,根據(jù)參數(shù)指定類的名字,返回對應的Class對象的引用。
? loadClass()這個方法是實現(xiàn)雙親模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題,因此我們最好是在雙親委派模型框架內(nèi)進行小范圍的改動,不破壞原有的穩(wěn)定結構。同時,也避免了自己重寫loadClass()方法的過程中必須寫雙親委托的重復代碼,從代碼的復用性來看,不直接修改這個方法始終是比較好的選擇。
? 當編寫好自定義類加載器后,便可以在程序中調(diào)用loadClass()方法來實現(xiàn)類加載操作。
3.3.3 說明
? 其父類加載器是系統(tǒng)類加載器 ? JVM中的所有類加載都會使用java.lang.ClassLoader.loadClass(String)接口(自定義類加載器并重寫java.lang.ClassLoader.loadClass(String)接口的除外),連JDK的核心類庫也不能例外。下面來看一下例子:
package com.fanhf.study.classLoader;import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException;public class MyClassLoader extends ClassLoader{private String byteCodePath;public MyClassLoader(String byteCodePath){this.byteCodePath = byteCodePath;}public MyClassLoader(ClassLoader parent, String byteCodePath) {super(parent);this.byteCodePath = byteCodePath;}@Overrideprotected Class<?> findClass(String className) {BufferedInputStream bis = null;ByteArrayOutputStream baos = null;try {//獲取字節(jié)碼文件的完整路徑String fileName = byteCodePath + className + ".class";//獲取一個輸入流bis = new BufferedInputStream(new FileInputStream(fileName));//獲取一個輸出流baos = new ByteArrayOutputStream();//具體讀入數(shù)據(jù)并寫出的過程int len;byte[] data = new byte[1024];while ((len = bis.read(data)) != -1) {baos.write(data, 0, len);}//獲取內(nèi)存中完成的字節(jié)數(shù)組的數(shù)據(jù)byte[] byteCodes = baos.toByteArray();//調(diào)用defineClass講字節(jié)數(shù)組轉(zhuǎn)換為class實例Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);return clazz;} catch (IOException e) {e.printStackTrace();} finally {try {if (bis != null) {bis.close();}} catch (IOException e) {e.printStackTrace();}try {if (baos != null) {baos.close();}} catch (IOException e) {e.printStackTrace();}}return null;} }測試類
package com.fanhf.study.classLoader;public class MyClassLoaderTest{public static void main(String[] args) {MyClassLoader loader = new MyClassLoader("/Users/fanhuifang/IdeaProject/MyProjects/java-study/target/classes/com/fanhf/study/classLoader/");Class clazz = null;try {clazz = loader.loadClass("MyClassLoader");System.out.println("加載此類的類加載器為:" + clazz.getClassLoader().getClass().getName());System.out.println("加載當前Test類的類加載器的父類加載器為:" + clazz.getClassLoader().getParent().getClass().getName());} catch (ClassNotFoundException e) {e.printStackTrace();}} }到此,本文就差不多告一段落了。
特此說明,本文中的內(nèi)容出處來自于《深入了解java虛擬機第二版》以及尚硅谷的康師傅的jvm課程,感興趣的小伙伴也可以再去看看,定會受益匪淺。
-----------------你知道的越多,不知道的越多------------------
總結
以上是生活随笔為你收集整理的java类加载和双亲委派模型浅说的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开源数据大屏AJ-Report
- 下一篇: 欧姆龙WINCC通ModbusTCP驱动