java类加载器、双亲委派、沙箱安全机制全都让你整明白(三万字,收藏慢慢啃)
目錄
?
一、概述
1、類加載的分類
2、類加載器的必要性
3、命名空間
4、類加載機制的基本特征
二、類加載器的分類
1、引導類加載器
2、擴展類加載器
3、系統類加載器
4、用戶自定義類加載器
三、測試不同的類加載器
四、ClassLoader源碼解析
1、抽象類ClassLoader的主要方法:(內部沒有抽象方法)
(1)public final ClassLoader getParent()
(2)public Class loadClass(String name) throws ClassNotFoundException
(3)protected Class findClass(String name) throws ClassNotFoundException
(4)protected final Class defineClass(byte[] b, int off, int len)
(5)protected final void resolveClass(Class c)
(6)protected final Class findLoadedClass(String name)
(7)private?final?ClassLoader?parent;
2、SecureClassLoader?與?URLClassLoader
3、ExtClassLoader?與?AppClassLoader
4、Class.forName()?與ClassLoader.loadClass()
五、雙親委派模型
1、定義與本質
2、優勢與劣勢
(1)雙親委派機制優勢
(2)代碼支持
(3)舉例
(4)思考
(5)雙親委托模式的弊端
(6)結論
3、破壞雙親委派機制
(1)破壞雙親委派機制1
(2)破壞雙親委派機制2
(3)破壞雙親委派機制3
4、熱替換的實現
六、沙箱安全機制
1、JDK1.0時期
2、JDK1.1時期
3、JDK1.2時期
4、JDK1.6時期
七、自定義類的加載器
1、為什么要自定義類加載器?
(1)隔離加載類
(2)修改類加載的方式
(3)擴展加載源
(4)防止源碼泄漏
2、常見的場景
3、注意
4、實現方式
(1)實現方式
(2)對比
八、java9新特性
一、概述
? ? 類加載器是JVM執行類加載機制的前提。
ClassLoader的作用:
? ? ClassLoader是Java的核心組件,所有的Class都是由ClassLoader進行加載的,ClassLoader負責通過各種方式將Class信息的二進制數據流讀入JVM內部,轉換為一個與目標類對應的java.lang.Class對象實例。然后交給Java虛擬機進行鏈接、初始化等操作。因此,ClassLoader在整個裝載階段,只能影響到類的加載,而無法通過ClassLoader去改變類的鏈接和初始化行為。至于它是否可以運行,則由Execution?Engine決定。
? ? 類加載器最早出現在Java1.0版本中,那個時候只是單純地為了滿足Java?Applet應用而被研發出來。但如今類加載器卻在OSGi(熱部署)、字節碼加解密領域大放異彩。這主要歸功于Java虛擬機的設計者們當初在設計類加載器的時候,并沒有考慮將它綁定在JVM內部,這樣做的好處就是能夠更加靈活和動態地執行類的加載操作。
1、類加載的分類
?? ?類的加載分類:顯式加載vs隱式加載
?? ?class文件的顯式加載與隱式加載的方式是指JVM加載class文件到內存的方式。
?? ?①?顯式加載指的是在代碼中通過調用ClassLoader加載class對象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對象。
?? ?②?隱式加載則是不直接在代碼中調用ClassLoader的方法加載class對象,而是通過虛擬機自動加載到內存中,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象,此時額外引用的類將通過JVM自動加載到內存中。(User u = new User(); //?隱式加載User類)
?? ?在日常開發以上兩種方式一般會混合使用。
2、類加載器的必要性
? ? 一般情況下,Java開發人員并不需要在程序中顯式地使用類加載器,但是了解類加載器的加載機制卻顯得至關重要。從以下幾個方面說:
? ? ①?避免在開發中遇到java.lang.ClassNotFoundException異?;騤ava.lang.NoClassDefFoundError異常時,手足無措。只有了解類加載器的加載機制才能夠在出現異常的時候快速地根據錯誤異常日志定位問題和解決問題。
? ? ②?需要支持類的動態加載或需要對編譯后的字節碼文件進行加解密操作時,就需要與類加載器打交道了。
? ? ③?開發人員可以在程序中編寫自定義類加載器來重新定義類的加載規則,以便實現一些自定義的處理邏輯。
3、命名空間
(1)何為類的唯一性?
? ? 對于任意一個類,都需要由加載它的類加載器和這個類本身一同確認其在Java虛擬機中的唯一性。每一個類加載器,都擁有一個獨立的類名稱空間:比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則,即使這兩個類源自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個類就必定不相等。
//創建自定義類加載器1 UserClassLoader loader1 = new UserClassLoader(); Class clazz1 = loader1.findClass("com.xiang.java.User"); //創建自定義類加載器2 UserClassLoader loader2 = new UserClassLoader(); Class clazz2 = loader2.findClass("com.xiang.java.User");System.out.println(clazz1 == clazz2); // false clazz1與clazz2對應了不同的類模板結構 System.out.println(clazz1.getClassLoader()); System.out.println(clazz1.getClassLoader()); // 這兩個ClassLoader的地址是不一樣的// 使用系統類加載器加載 Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.xiang.java.User"); System.out.println(clazz3.getClassLoader());(2)命名空間
? ? ①?每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成。
? ? ②?在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
? ? ③?在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
? ? 在大型應用中,我們往往借助這一特性,來運行同一個類的不同版本。
4、類加載機制的基本特征
? ? 通常類加載機制有三個基本特征:
? ? ①?雙親委派模型。但不是所有類加載都遵守這個模型,有的時候,啟動類加載器所加載的類型,是可能要加載用戶代碼的,比如JDK內部的ServiceProvider/ServiceLoader機制,用戶可以在標準API框架上,提供自己的實現,JDK也需要提供些默認的參考實現。例如,Java中JNDI、JDBC、文件系統、Cipher等很多方面,都是利用的這種機制,這種情況就不會用雙親委派模型去加載,而是利用所謂的上下文加載器。
? ? ②?可見性,子類加載器可以訪問父加載器加載的類型,但是反過來是不允許的。不然,因為缺少必要的隔離,我們就沒有辦法利用類加載器去實現容器的邏輯。
? ? ③?單一性,由于父加載器的類型對于子加載器是可見的,所以父加載器加載過的類型,就不會在子加載器中重復加載。但是注意,類加載器“鄰居”間,同一類型仍然可以被加載多次,因為互相并不可見。
二、類加載器的分類
? ? JVM支持兩種類型的類加載器,分別為引導類加載器(Bootstrap?ClassLoader)和自定義類加載器(User-Defined?ClassLoader)。
? ? 從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是Java虛擬機規范卻沒有這么定義,而是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器。無論類加載器的類型如何劃分,在程序中我們最常見的類加載器結構主要是如下情況:
? ? ·?除了頂層的啟動類加載器外,其余的類加載器都應當由自己的“父類”加載器。
? ? ·?不同類加載器看似是繼承(Inheritance)關系,實際上是包含關系。在下層加載器中,包含著上層加載器的引用。
class ClassLoader {ClassLoader parent; // 父類加載器public ClassLoader(ClassLoader parent) {this.parent = parent;} }class ParentClassLoader extends ClassLoader {public ParentClassLoader(ClassLoader parent) {super(parent);} }class ChildClassLoader extends ClassLoader {public ChildClassLoader(ClassLoader parent) { // parent = new ParentClassLoader();super(parent);} }1、引導類加載器
啟動類加載器(引導類加載器,Bootstrap?ClassLoader)
·?這個類加載使用C/C++語言實現的,嵌套在JVM內部。
·?它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內容)。用于提供JVM自身需要的類。
·?并不繼承自java.lang.ClassLoader,沒有父加載器。
·?出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類。
·?加載擴展類和應用程序類加載器,并指定為他們的父類加載器。
使用-XX:+TraceClassLoading參數得到:
2、擴展類加載器
擴展類加載器(Extension?ClassLoader)
·?Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現。
·?繼承于ClassLoader類。
·?父類加載器為啟動類加載器。
·?從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載。
// 獲取啟動類加載器、擴展類加載器加載的路徑 import sun.misc.Launcher; import sun.security.ec.CurveDB; import java.net.URL; import java.security.Provider;public class ClassLoaderTest {public static void main(String[] args) {System.out.println("*****************啟動類加載器*****************");// 獲取BootstrapClassLoader能夠加載的api的路徑URL[] urls = Launcher.getBootstrapClassPath().getURLs();for (URL element : urls) {System.out.println(element.toExternalForm());}//從上面路徑中隨意選擇一個類,來看看他的類加載器是什么:引導類加載器ClassLoader classLoader = Provider.class.getClassLoader();System.out.println(classLoader); // null 為引導類加載器System.out.println("*****************擴展類加載器*****************");String extDirs = System.getProperty("java.ext.dirs");for (String path : extDirs.split(";")) {System.out.println(path);}//從上面路徑中隨意選擇一個類,來看看他的類加載器是什么:擴展類加載器ClassLoader classLoader1 = CurveDB.class.getClassLoader();System.out.println(classLoader1); // sun.misc.Launcher$ExtClassLoader@532760d8} }*****************啟動類加載器***************** file:/F:/Java/jdk1.8.0_131/jre/lib/resources.jar file:/F:/Java/jdk1.8.0_131/jre/lib/rt.jar file:/F:/Java/jdk1.8.0_131/jre/lib/sunrsasign.jar file:/F:/Java/jdk1.8.0_131/jre/lib/jsse.jar file:/F:/Java/jdk1.8.0_131/jre/lib/jce.jar file:/F:/Java/jdk1.8.0_131/jre/lib/charsets.jar file:/F:/Java/jdk1.8.0_131/jre/lib/jfr.jar file:/F:/Java/jdk1.8.0_131/jre/classes null *****************擴展類加載器***************** F:\Java\jdk1.8.0_131\jre\lib\ext C:\Windows\Sun\Java\lib\ext sun.misc.Launcher$ExtClassLoader@532760d83、系統類加載器
應用程序類加載器(系統類加載器,AppClassLoader)
·?Java語言編寫,由sun.misc.Launcher$AppClassLoader實現。
·?繼承于ClassLoader類。
·?父類加載器為擴展類加載器。
·?它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫。
·?應用程序中的類加載器默認是系統類加載器。
·?它是用戶自定義類加載器的默認父加載器。
·?通過ClassLoader的getSystemClassLoader()方法可以獲取到該類加載器。
4、用戶自定義類加載器
·?在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的。在必要時,我們還可以自定義類加載器,來定制類的加載方式。
·?體現Java語言強大生命力和巨大魅力的關鍵因素之一便是,Java開發者可以自定義類加載器來實現類庫的動態加載,加載源是可以本地的JAR包,也可以是網絡上的遠程資源。
·?通過類加載器可以實現非常絕妙的插件機制,這方面的實際應用案例舉不勝舉。例如,著名的OSGI組件框架,再如Eclipse的插件機制。類加載器為應用程序提供了一種動態增加新功能的機制,這種機制無須重新打包發布應用程序就能實現。
·?同時,自定義加載器能夠實現應用隔離,例如?Tomcat,Spring等中間件和組件框架都在內部實現了自定義的加載器,并通過自定義加載器隔離不同的組件模塊。這種機制比C/C++程序要好太多,想不修改C/C++程序就能為其新增功能,幾乎是不可能的,僅僅一個兼容性便能阻擋住所有美好的設想。
·?自定義類加載器通常需要繼承于ClassLoader。
三、測試不同的類加載器
?? ?每個Class對象都會包含一個定義它的ClassLoader的一個引用。
?? ?獲取ClassLoader的途徑:
? ? ·?獲得當前類的ClassLoader:clazz.getClassLoader()
? ? ·?獲得當前線程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
? ? ·?獲得系統的ClassLoader:ClassLoader.getSystemClassLoader()
說明:
? ? 站在程序的角度看,引導類加載器與另外兩種類加載器(系統類加載器和擴展類加載器)并不是同一個層次意義上的加載器,引導類加載器是使用C++語言編寫而成的,而另外兩種類加載器則是使用Java語言編寫而成的。由于引導類加載器壓根兒就不是一個Java類,因此在Java程序中只能打印出空值。
? ? 數組類的Class對象,不是由類加載器去創建的,而是在Java運行期JVM根據需要自動創建的。對于數組類的類加載器來說,是通過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是一樣的;如果數組當中的元素類型是基本數據類型,數組類是沒有類加載器的。
public class ClassLoaderTest1 {public static void main(String[] args) throws ClassNotFoundException {// 獲取系統類加載器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2// 獲取擴展類加載器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@277c0f21// 獲取引導類加載器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println(bootstrapClassLoader); // null// 獲取指定類的加載器ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();System.out.println(classLoader); // null// 自定義的類默認是系統類加載器ClassLoader classLoader1 = ClassLoaderTest1.class.getClassLoader();System.out.println(classLoader1); // sun.misc.Launcher$AppClassLoader@18b4aac2// 關于數組的加載,與數組當中元素類型的類加載器是一樣的String[] arrStr = new String[10];System.out.println(arrStr.getClass()); // class [Ljava.lang.String;System.out.println(arrStr.getClass().getClassLoader()); // null,表示使用的是引導類加載器ClassLoaderTest1[] arr1 = new ClassLoaderTest1[10];System.out.println(arr1.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2//基本數據類型,不需要加載的,這個null并不是用了引導類加載器,而是沒有類加載器int[] arr2 = new int[10];System.out.println(arr2.getClass().getClassLoader());// null// 線程上下文的類加載器System.out.println(Thread.currentThread().getContextClassLoader());// sun.misc.Launcher$AppClassLoader@18b4aac2} }四、ClassLoader源碼解析
ClassLoader與現有類加載器的關系:
? ? 除了以上虛擬機自帶的加載器外,用戶還可以定制自己的類加載器。Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應該繼承ClassLoader類。
?
?
1、抽象類ClassLoader的主要方法:(內部沒有抽象方法)
(1)public final ClassLoader getParent()
? ? 返回該類加載器的超類加載器。
(2)public Class<?> loadClass(String name) throws ClassNotFoundException
? ? 加載名稱為name的類,返回結果為java.lang.Class類的實例。如果找不到類,則返回ClassNotFoundException異常。該方法中的邏輯就是雙親委派模式的實現。
protected Class<?> loadClass(String name, boolean resolve) // resolve為true時,加載class的同時進行解析操作,默認為falsethrows ClassNotFoundException {synchronized (getClassLoadingLock(name)) { // 同步操作,保證只能加載一次// 首先,在緩存中判斷是否已經加載同名的類。Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// 獲取當前類加載器的父類加載器if (parent != null) {// 如果存在父類加載器,則調用父類加載器,進行類加載(進行遞歸)。(雙親委派機制)c = parent.loadClass(name, false);} else { // parent == null ,父類加載器是引導類加載器c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) { // 當前類的加載器的父類加載器未加載此類 or 當前類的加載器未加載此類// 調用當前ClassLoader的findClass方法。long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) { // 是否進行解析操作resolveClass(c);}return c;} }(3)protected Class<?> findClass(String name) throws ClassNotFoundException
? ? 查找二進制名稱為name的類,返回結果為java.lang.Class類的實例。這是一個受保護的方法,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委托機制,該方法會在檢查完父類加載器之后被loadClass()方法調用。
? ? 在JDK1.2之前,在自定義類加載時,總會去繼承ClassLoader類并重寫loadClass方法,從而實現自定義的類加載類。但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗后,則會調用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。
? ? 需要注意的是ClassLoader類中并沒有實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的。一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規則,取得要加載類的字節碼后轉換成流,然后調用defineClass()方法生成類的Class對象。
// ClassLoader類中的findClass方法直接拋出異常。 protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name); }// 在URLClassLoader類中重寫了findClass方法。 protected Class<?> findClass(final String name)throws ClassNotFoundException {final Class<?> result;try {result = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {public Class<?> run() throws ClassNotFoundException {String path = name.replace('.', '/').concat(".class");Resource res = ucp.getResource(path, false);if (res != null) {try {return defineClass(name, res);} catch (IOException e) {throw new ClassNotFoundException(name, e);}} else {return null;}}}, acc);} catch (java.security.PrivilegedActionException pae) {throw (ClassNotFoundException) pae.getException();}if (result == null) {throw new ClassNotFoundException(name);}return result; }(4)protected final Class<?> defineClass(byte[] b, int off, int len)
? ? 根據給定的字節數組b轉換為Class的實例,off和len參數表示實際Class信息在byte數組中的位置和長度,其中byte數組b是ClassLoader從外部獲取的。這是受保護的方法,只有在自定義ClassLoader子類中可以使用。
? ? defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象(ClassLoader中已實現該方法邏輯),通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象,如通過網絡接收一個類的字節碼,然后轉換為byte字節流創建對應的Class對象。
? ? defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規則,取得要加載類的字節碼后轉換成流,然后調用defineClass()方法生成類的Class對象。
簡單舉例:
protected Class<?> findClass(String name) throws ClassNotFoundException {// 獲取類的字節數組byte[] classData = getClassDate(name);if(classData == null) {throw new ClassNotFoundException();} else {//使用defineClass生成class對象return defineClass(name, classData, 0, classData.length); } }(5)protected final void resolveClass(Class<?> c)
? ? 鏈接指定的一個Java類。使用該方法可以使用類的Class對象創建完成的同時也被解析。前面我們說鏈接階段主要是對字節碼進行驗證,為類變量分配內存并設置初始值同時將字節碼文件中的符號引用轉換為直接引用。
(6)protected final Class<?> findLoadedClass(String name)
? ? 查找名稱為name的已經被加載過的類,返回結果為java.lang.Class類的實例。這個方法是final方法,無法被修改。
(7)private?final?ClassLoader?parent;
? ? 它也是一個ClassLoader的實例,這個字段所表示的ClassLoader也稱為這個ClassLoader的雙親。在類加載的過程中,ClassLoader可能會將某些請求交予自己的雙親處理。
2、SecureClassLoader?與?URLClassLoader
? ? 接著SecureClassLoader擴展了ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,一般我們不會直接跟這個類打交道,更多的是與它的子類URLClassLoader有所關聯。
? ? 前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實現,比如findClass()、findResource()等。而URLClassLoader這個實現類為這些方法提供了具體的實現。并新增了URLClassPath類協助取得Class字節碼流等功能。在編寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
3、ExtClassLoader?與?AppClassLoader
? ? 這兩個類都繼承自URLClassLoader,是sun.misc.Launcher的靜態內部類。sun.misc.Launcher主要被系統用于啟動主應用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher創建的,其類主要類結構如下:
?
? ? 我們發現ExtClassLoader并沒有重寫loadClass()方法,這足以說明其遵循雙親委派模式,而AppClassLoader重載了loadClass()方法,但最終調用的還是父類loadClass()方法,因此依然遵守雙親委派模式。
4、Class.forName()?與ClassLoader.loadClass()
· Class.forName():是一個靜態方法,最常用的是Class.forName(String className);根據傳入的類的全限定名返回一個Class對象。該方法在將Class文件加載到內存的同時,會執行類的初始化。
·?ClassLoader.loadClass():這是一個實例方法,需要一個ClassLoader對象來調用該方法。該方法將Class文件加載到內存時,并不會執行類的初始化,直到這個類第一次使用時才進行初始化。該方法因為需要得到一個ClassLoader對象,所以可以根據需要指定使用哪個類加載器。
五、雙親委派模型
1、定義與本質
? ? 類加載器用來把類加載到Java虛擬機中。從JDK1.2版本開始,類的加載過程采用雙親委派機制,這種機制能更好地保證Java平臺的安全。
?? ?定義:如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回。只有父類加載器無法完成此加載任務時,才自己去加載。
? ? 本質:規定了類加載的順序是:引導類加載器先加載,若加載不到,由擴展類加載器加載,若還加載不到,才會由系統類加載器或自定義的類加載器進行加載。
?
2、優勢與劣勢
(1)雙親委派機制優勢
① 避免類的重復加載,確保一個類的全局唯一性。
? ? Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關系可以避免類的重復加載,當父親已經加載了該類,就沒有必要子ClassLoader再加載一次。
② 保護程序安全,防止核心API被隨意篡改。
(2)代碼支持
? ? 雙親委派機制在java.lang.ClassLoader.loadClass(String, boolean)方法中體現。該方法的邏輯如下:
①?先在當前加載器的緩存中查找有無目標類,如果有,直接返回。
②?判斷當前加載器的父加載器是否為空,如果不為空,則調用parent.loadClass(name, false)方法進行加載。
③?反之,如果當前加載器的父類加載器為空,則調用findBootstrapClassOrNull(name)方法,讓引導類加載器進行加載。
④?如果通過以上3條路徑都沒能成功加載,則調用findClass(name)方法進行加載。該接口最終會調用java.lang.ClassLoader方法的defineClass系列的native方法加載目標Java類。
? ? 雙親委派的模型就隱藏在這第2和第3步中。
(3)舉例
? ? 假設當前加載的是java.lang.Object這個類,很顯然,該類屬于JDK中核心類,因此一定只能由引導類加載器進行加載。
? ? 當JVM準備加載java.lang.Object時,JVM默認會使用系統類加載器去加載,按照上面4步加載的邏輯,在第1步從系統類的緩存中肯定查找不到該類,于是進入第2步。由于系統類加載器的父加載器是擴展類加載器,于是擴展類加載器繼續從第1步開始重復。由于擴展類加載器的緩存中也一定查找不到該類,因此進入第2步。擴展類的父加載器是null,因此系統調用findClass(String),最終通過引導類加載器進行加載。
(4)思考
? ? 如果在自定義的類加載器中重寫java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadClass(String, boolean)方法,抹去其中的雙親委派機制,僅保留上面這4步中的第1步與第4步,那么是不是就能夠加載核心類庫了呢?
? ? 這也不行!因為JDK還未核心類庫提供了一層保護機制。不管是自定義的類加載器,還是系統類加載器亦或擴展類加載器,最終都必須調用java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain)方法,而該方法會執行preDefineClass()方法,該方法中提供了對JDK核心類庫的保護。
(5)雙親委托模式的弊端
? ? 檢查類是否加載的委托過程是單向的,這個方式雖然從結構上說比較清晰,使各個ClassLoader的職責非常明確,但是同時會帶來一個問題,即頂層的ClassLoader無法訪問底層的ClassLoader所加載的類。
? ? 通常情況下,啟動類加載器中的類為系統核心類,包括一些重要的系統接口,而在應用類加載器中,為應用類。按照這種模式,應用類訪問系統類自然是沒有問題,但是系統類訪問應用類就會出現問題。比如在系統類中提供了一個接口,該接口需要在應用類中得以實現,該接口還綁定一個工廠方法,用于創建該接口的實例,而接口和工廠方法都在啟動類加載器中。這時,就會出現該工廠方法無法創建由應用類加載器加載的應用實例的問題。
(6)結論
? ? 由于Java虛擬機規范并沒有明確要求類加載器的加載機制一定要使用雙親委派模型,知識建議采用這種方式而已。
? ? 比如在Tomcat中,類加載器所采用的加載機制就和傳統的雙親委派模型有一定區別,當缺省的類加載器接收到一個類的加載任務時,首先會由它自行加載,當它加載失敗時,才會將類的加載任務委派給它的超類加載器去執行,這同時也是Servlet規范推薦的一種做法。
3、破壞雙親委派機制
? ? 雙親委派模型并不是一個具有強制性約束的模型,而是Java設計者推薦給開發者們的類加載器實現方式。
? ? 在Java的世界中大部分的類加載器都遵循這個模型,但也有例外的情況,直到Java模塊化出現為止,雙親委派模型主要出現過3次較大規模“被破壞”的情況。
(1)破壞雙親委派機制1
? ??
? ? 第一次破壞雙親委派機制:
? ? 雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK1.2面世以前的“遠古”時代。
? ? 由于雙親委派模型在JDK1.2之后才被引入,但是類加載器的概念和抽象類java.lang.ClassLoader則在Java的第一個版本中就已經存在,面對已經存在的用戶自定義類加載器的代碼,Java設計者們引入雙親委派模型時不得不作出一些妥協,為了兼容這些已有代碼,無法再以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一個新的protected方法findClass(),并引導用戶編寫的類加載邏輯盡可能去重寫這個方法,而不是在loadClass()中編寫代碼。上面我們已經分析過loadClass()方法,雙親委派的具體邏輯就實現在這里面,按照loadClass()方法的邏輯,如果父類加載失敗,會自動調用自己的findClass()方法來完成加載,這樣既不影響用戶按照自己的意愿去加載類,又可以保證新寫出來的類加載器是符合雙親委派規則的。
(2)破壞雙親委派機制2
? ? 第二次破壞雙親委派機制:線程上下文類加載器。
? ? 雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,雙親委派很好地解決了各個類加載器協作時基礎類型的一致性問題(越基礎的類越上層的加載器進行加載),基礎類型之所以被稱為“基礎”,是因為它們總是作為被用戶代碼繼承、調用的API存在,但程序設計往往沒有絕對不變的完美規則,如果有基礎類型又要調用會用戶的代碼,那該怎么辦呢?
? ? 這并非是不可能出現的事情,一個典型的例子便是JNDI服務,JDNI現在已經是Java的標準服務,它的代碼由啟動類加載器來完成加載(在JDK1.3時加入到rt.jar的),肯定屬于Java中很基礎的類型了。但JNDI存在的目的就是對資源進行查找和集中管理,它需要調用由其他廠商實現并部署在應用程序的ClassPath下的JNDI服務提供接口(Service?Provider?Interface,?SPI)的代碼,現在問題來了,啟動類加載器是絕不可能認識、加載這些代碼的,那該怎么辦?(SPI:在Java平臺中,通常把核心類rt.jar中提供外部服務、可由應用層自行實現的接口稱為SPI)
? ? 為了解決這個困境,Java的設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread?Context?ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
? ? 有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則,但也是無可奈何的事情。Java中涉及SPI的加載基本上都采用這種方式來完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不過,當SPI的服務提供者多于一個的時候,代碼就只能根據具體提供者的類型來硬編碼判斷,為了消除這種極不優雅的實現方式,在JDK6時,JDK提供了java.util.ServiceLoader類,以META-INF/services中的配置信息,輔以責任鏈模式,這才算是給SPI的加載提供了一種相對合理的解決方案。
? ? 默認上下文加載器就是應用類加載器,這樣以上下文加載器為中介,使得啟動類加載器中的代碼也可以訪問應用類加載器中的類。
(3)破壞雙親委派機制3
? ? 第三次破壞雙親委派機制:
? ? 雙親委派模型的第三次“被破壞”是由于用戶對程序動態性的追求而導致的。如:代碼熱替換(Hot?Swap)、模塊熱部署(Hot?Deployment)等。
? ? IBM公司主導的JSR-291(即OSGi?R4.2)實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現,每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加復雜的網絡結構。
? ? 當收到類加載請求時,OSGi將按照下面的順序進行類搜索:
①?將以java.*開頭的類,委派給父類加載器加載。
②?否則,將委派列表名單內的類,委派給父類加載器加載。
③?否則,將Import列表中的類,委派給Export這個類的Bundle的類加載器加載。
④?否則,查找當前Bundle的ClassPath,使用自己的類加載器加載。
⑤?否則,查找類是否在自己的Fragment?Bundle中,如果在,則委派給Fragment?Bundle的類加載器加載。
⑥?否則,查找Dynamic?Import列表的Bundle,委派給對應的Bundle的類加載器加載。
⑦?否則,類查找失敗。
? ? 說明:只有開頭兩點仍然符合雙親委派模型的原則,其余的類查找都是在平級的類加載器中進行的。
? ? 小結:這里,我們使用了“被破壞”這個詞來形容上述不符合雙親委派模型原則的行為,但這里“被破壞”并不一定是帶有貶義的。只要有明確的目的和充分的理由,突破舊有原則無疑是一種創新。
? ? 正如:OSGi中的類加載器的設計不符合傳統的雙親委派的類加載器架構,且業界對其為了實現熱部署而帶來的額外的高復雜度還存在不少爭議,但對這方面有了解的技術人員基本還是能達成一個共識,認為OSGi中對類加載器的運用是值得學習的,完全弄懂了OSGi的實現,就算是掌握了類加載器的精髓。
?? ?
4、熱替換的實現
? ? 熱替換是指在程序的運行過程中,不停止服務,只通過替換程序文件來修改程序的行為。熱替換的關鍵需求在于服務不能中斷,修改必須立即表現正在運行的系統之中?;旧洗蟛糠帜_本語言都是天生支持熱替換的,比如:PHP,只要替換了PHP源文件,這種改動就會立即生效,而無需重啟Web服務器。
? ? 但對Java來說,熱替換并非天生就支持,如果一個類已經加載到系統中,通過修改類文件,并無法讓系統再來加載并重定義這個類。因此,在Java中實現這一功能的一個可行的方法就是靈活運用ClassLoader。
? ? 注意:由不同ClassLoader加載的同名類屬于不同的類型,不能相互轉換和兼容。即兩個不同的ClassLoader加載同一個類,在虛擬機內部,會認為這2個類是完全不同的。
? ? 根據這個特點,可以用來模擬熱替換的實現,基本思路如下圖所示:
import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; /** * 自定義類加載器 */ public class MyClassLoader extends ClassLoader {private String rootDir;public MyClassLoader(String rootDir) {this.rootDir = rootDir;}@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {Class clazz = this.findLoadedClass(className);FileChannel fileChannel = null;WritableByteChannel outChannel = null;if(null == clazz) {try {String classFile = getClassFile(className);FileInputStream fis = new FileInputStream(classFile);fileChannel = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();outChannel = Channels.newChannel(baos);ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (true) {int i = fileChannel.read(buffer);if(i == 0 || i == -1){break;}buffer.flip();outChannel.write(buffer);buffer.clear();}byte[] bytes = baos.toByteArray();clazz = defineClass(className, bytes, 0, bytes.length);} catch (FileNotFoundException e){e.printStackTrace();} catch (IOException e){e.printStackTrace();}try {if(outChannel != null) outChannel.close();} catch (IOException e) {e.printStackTrace();}}return clazz;}/*** 類文件的完全路徑*/private String getClassFile(String className) {return rootDir + "\\" + className.replace('.', '\\') + ".class";} }public class Demo1 {public void hot(){System.out.println("OldDemo1-- > newDemo1");} }import java.lang.reflect.Method; /** * 熱部署實例,修改Demo1之后重新編譯,就會自動調用新Demo1的方法 */ public class HotBushu {public static void main(String[] args) {while (true) {// 每隔5秒鐘獲取一次類try {// 1.創建自定義類加載器MyClassLoader loader = new MyClassLoader("E:\\target\\classes");// 2.加載指定的類Class clazz = loader.findClass("com.myUtils.test.classloaderTest.Demo1");// 3.創建運行時類的實例Object demo = clazz.newInstance();// 4.調用hot方法Method m = clazz.getMethod("hot");m.invoke(demo);Thread.sleep(5000);} catch (Exception e) {System.out.println("not find");try {Thread.sleep(5000);} catch (InterruptedException ex) {ex.printStackTrace();}}}} }六、沙箱安全機制
·?保證程序安全
·?保護Java原生的JDK代碼
?? ?Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?沙箱是一個限制程序運行的環境。
? ? 沙箱機制就是將Java代碼限定在虛擬機(JVM)特定的運行范圍中,并且嚴格限制代碼對本地系統資源訪問。通過這樣的措施來保證對代碼的有限隔離,防止對本地系統造成破壞。
? ? 沙箱主要限制系統資源訪問,那系統資源包括什么?CPU、內存、文件系統、網絡。不同級別的沙箱對這些資源訪問的限制也可以不一樣。
? ? 所有的Java程序運行都可以指定沙箱,可以定制安全策略。
1、JDK1.0時期
? ? 在Java中將執行程序分成本地代碼和遠程代碼兩種,本地代碼默認視為可信任的,而遠程代碼則被看作是不受信的。對于受信的本地代碼,可以訪問一切本地資源。而對于非授信的遠程代碼在早期的Java實現中,完全依賴于沙箱(Sandbox)機制。如下圖所示JDK1.0安全模型。
2、JDK1.1時期
? ? JDK1.0中如此嚴格的安全機制也給程序的功能擴展帶來障礙,比如當用戶希望遠程代碼訪問本地系統的文件時候,就無法實現。
? ? 因此在后續的Java1.1版本中,針對安全機制做了改進,增加了安全策略。允許用戶指定代碼對本地資源的訪問權限。如下圖所示JDK1.1安全模型。
3、JDK1.2時期
? ? 在Java1.2版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。如下圖所示JDK1.2安全模型。
4、JDK1.6時期
? ? 當前最新的安全機制實現,則引入了域(Domain)的概念。
? ? 虛擬機會把所有代碼加載到不同的系統域和應用域。系統域部分專門負責與關鍵資源進行交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機中不同的受保護域(Protected?Domain),對應不一樣的權限(Permission)。存在于不同域中的類文件就具有了當前域的全部權限,如下圖所示,最新的安全模型(JDK1.6)
七、自定義類的加載器
1、為什么要自定義類加載器?
(1)隔離加載類
? ? 在某些框架內進行中間件與應用的模塊隔離,把類加載到不同的環境。比如:阿里內某容器框架通過自定義類加載器確保應用中依賴的jar包不會影響到中間件運行時使用的jar包。再比如:Tomcat這類Web應用服務器,內部自定義了好幾種類加載器,用于隔離同一個Web應用服務器上的不同應用程序。
(2)修改類加載的方式
? ? 類的加載模型并非強制,除Bootstrap外,其他的加載并非一定要引入,或者根據實際情況在某個時間點進行按需進行動態加載。
(3)擴展加載源
? ? 比如從數據庫、網絡、甚至是電視機機頂盒進行加載。
(4)防止源碼泄漏
? ? Java代碼容易被編譯和篡改,可以進行編譯加密。那么類加載也需要自定義,還原加密的字節碼。
2、常見的場景
(1)實現類似進程內隔離,類加載器實際上用作不同的命名空間,以提供類似容器、模塊化的效果。例如,兩個模塊依賴于某個類庫的不同版本,如果分別被不同的容器加載,就可以互不干擾。這個方面的集大成者是Java?EE和OSGi、JPMS等框架。
(2)應用需要從不同的數據源獲取類定義信息,例如網絡數據源,而不是本地文件系統。或者是需要自己操縱字節碼,動態修改或者生成類型。
3、注意
? ? 在一般情況下,使用不同的類加載器去加載不同的功能模塊,會提高應用程序的安全性。但是,如果涉及Java類型轉換,則加載器反而容易產生不美好的事情。在做Java類型轉換時,只有兩個類型都是由同一個加載器所加載,才能進行類型轉換,否則轉換時會發生異常。
4、實現方式
?? ?用戶通過定制自己的類加載器,這樣可以重新定義類的加載規則,以便實現一些自定義的處理邏輯。
(1)實現方式
? ? Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應該繼承ClassLoader類。
? ? 在自定義ClassLoader的子類的時候,我們常見的會有兩種做法:
? ? ①?方式一:重寫loadClass()方法
? ? ②?方式二:重寫findClass()方法
(2)對比
? ? 這兩種方法本質上差不多,畢竟loadClass()也會調用findClass(),但是從邏輯上講我們最好不要直接修改loadClass()的內部邏輯。建議的做法是只在findClass()里重寫自定義類的加載方法,根據參數指定類的名字,返回對應的Class對象的引用。
? ? loadClass()方法是實現雙親委派模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此我們最好是在雙親委派模型框架內進行小范圍的改動,不破壞原有的穩定結構。同時,也避免了自己重寫loadClass()方法的過程中必須寫雙親委托的重復代碼,從代碼的復用性來看,不直接修改這個方法始終是比較好的選擇。
? ? 當編寫好自定義類加載器后,便可以在程序中調用loadClass()方法來實現類的加載操作。
import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException;/** * 自定義類加載器2 */ public class MyClassLoader2 extends ClassLoader {private String rootDir;public MyClassLoader2(String rootDir) {this.rootDir = rootDir;}public MyClassLoader2(ClassLoader parent, String rootDir) {super(parent);this.rootDir = rootDir;}@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {BufferedInputStream bis = null;ByteArrayOutputStream baos = null;try {// 獲取字節碼文件的完整路徑String fileName = getClassFile(className);// 獲取輸入流bis = new BufferedInputStream(new FileInputStream(fileName));// 獲取輸出流baos = new ByteArrayOutputStream();// 具體讀入數據、寫出數據int len;byte[] data = new byte[1024];while((len = bis.read(data)) != -1) {baos.write(data, 0, len);}// 獲取內存中完整的字節數組數據byte[] byteCodes = baos.toByteArray();// 調用defineClass,將字節數組數據轉換為Class實例return defineClass(null, byteCodes, 0, byteCodes.length);} catch (IOException e) {e.printStackTrace();} finally {try {if(baos != null) {baos.close();}if(bis != null) {bis.close();}} catch (IOException e) {e.printStackTrace();}}return null;}/*** 類文件的完全路徑*/private String getClassFile(String className) {return rootDir + "\\" + className.replace('.', '\\') + ".class";}public static void main(String[] args) throws ClassNotFoundException {MyClassLoader2 loader = new MyClassLoader2("E:\\");Class clazz = loader.findClass("com.myUtils.test.classloaderTest.Demo1");System.out.println("此class的類加載器為:" + clazz.getClassLoader());System.out.println("此class的類加載器父類為:" + clazz.getClassLoader().getParent());} }此class的類加載器為:com.myUtils.test.classloaderTest.MyClassLoader2@277c0f21 此class的類加載器父類為:sun.misc.Launcher$AppClassLoader@18b4aac2八、java9新特性
?? ?為了保證兼容性,JDK9沒有從根本上改變三層類加載器架構和雙親委派模型,但為了模塊化系統的順利運行,仍然發生了一些值得被注意的變動。
1、擴展機制被移除,擴展類加載器由于向后兼容性的原因被保留,不過被重命名為平臺類加載器(platform?class?loader)。可以通過ClassLoader的新方法getPlatformClassLoader()來獲取。
? ? JDK9時基于模塊化進行構建(原來的rt.jar和tools.jar被拆分成數十個JMOD文件),其中的Java類庫就已天然地滿足了可擴展的需求,那自然無須在保留<JAVA_HOME>\lib\ext目錄,此前使用這個目錄或者java.ext.dirs系統變量來擴展JDK功能的機制已經沒有繼續存在的價值了。
2、平臺類加載器和應用程序類加載器都不再繼承自java.net.URLClassLoader。
? ? 現在啟動類加載器、平臺類加載器、應用程序類加載器全部繼承于jdk.internal.loader.BuiltinClassLoader。
? ? 如果有程序直接依賴了這種繼承關系,或者依賴了URLClassLoader類特定方法,那代碼很可能會在JDK9及更高版本的JDK中崩潰。
3、在Java9中,類加載器有了名稱。該名稱在構造方法中指定,可以通過getName()方法來獲取。平臺類加載器的名稱是platform,應用類加載器的名稱是app。類加載器的名稱在調試與類加載器相關的問題時會非常有用。
4、啟動類加載器現在是在jvm內部和java類庫共同協作實現的類加載器(以前是C++實現),但為了與之前代碼兼容,在獲取啟動類加載器的場景中仍然會返回null,而不會得到BootClassLoader實例。
5、類加載的委派關系也發生了變動。
? ? 當平臺及應用程序類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統模塊中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個模塊的加載器完成加載。
?
6、附加
? ? 在Java模塊化系統明確規定了三個類加載器負責各自加載的模塊:
(1)啟動類加載器負責加載的模塊
(2)平臺類加載器負責加載的模塊
(3)應用程序類加載器負責加載的模塊
總結
以上是生活随笔為你收集整理的java类加载器、双亲委派、沙箱安全机制全都让你整明白(三万字,收藏慢慢啃)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 超详细!带输入输出的十五至尊图你见过吗?
- 下一篇: spring系列-注解驱动原理及源码-A