加载类_JVM类加载详解
類的加載器
概述
類加載器是JVM執行類加載機制的前提。
ClassLoader的作用:ClassLoader是Java的核心組件,所有的Class都是由ClassLoader進行加載的,ClassLoader負責通過各種方式將Class信息的二進制數據流讀入JVM內部,轉換為一個與目標類對應的java.lang.Class對象實例。然后交給Java虛擬機進行鏈接、初始化等操作。因此,ClassLoader在整個裝載階段,只能影響到類的加載,而無法通過ClassLoader去改變類的鏈接和初始化行為。至于它是否可以運行,則由Execution Engine決定。
jvm類加載分類
類的加載分類:顯式加載 vs 隱式加載
class文件的顯式加載與隱式加載的方式是指JVM加載class文件到內存的方式。
- 顯式加載指的是在代碼中通過調用ClassLoader加載class對象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對象。
- 隱式加載則是不直接在代碼中調用ClassLoader的方法加載class對象,而是通過虛擬機自動加載到內存中,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象,此時額外引用的類將通過JVM自動加載到內存中。
代碼示例:
User?user=new?User();//隱式加載Class?clazz=Class.forName("com.atguigu.java.User");//顯式加載并初始化
ClassLoader.getSystemClassLoader().loadClass("T1.Parent");?//顯式加載,但不初始化
類加載器的必要性
一般情況下,Java開發人員并不需要在程序中顯式地使用類加載器,但是了解類加載器的加載機制卻顯得至關重要。從以下幾個方面說:
- 避免在開發中遇到java.lang.ClassNotFoundException異常或java.lang.NoClassDefFoundError異常時,手足無措。只有了解類加載器的 加載機制才能夠在出現異常的時候快速地根據錯誤異常日志定位問題和解決問題
- 需要支持類的動態加載或需要對編譯后的字節碼文件進行加解密操作時,就需要與類加載器打交道了。
- 開發人員可以在程序中編寫自定義類加載器來重新定義類的加載規則,以便實現一些自定義的處理邏輯。
命名空間
什么是類的唯一性?
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確認其在Java虛擬機中的唯一性。每一個類加載器,都擁有一個獨立的類名稱空間:比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則,即使這兩個類源自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個類就必定不相等。
命名空間
- 每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成
- 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
- 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類
類加載機制的基本特征
- 雙親委派模型。但不是所有類加載都遵守這個模型,有的時候,啟動類加載器所加載的類型,是可能要加載用戶代碼的,比如JDK內部的ServiceProvider/ServiceLoader機制,用戶可以在標準API框架上,提供自己的實現,JDK也需要提供些默認的參考實現。例如,Java中JNDI、JDBC、文件系統、Cipher等很多方面,都是利用的這種機制,這種情況就不會用雙親委派模型去加載,而是利用所謂的上下文加載器。
- 可見性,子類加載器可以訪問父加載器加載的類型,但是反過來是不允許的。不然,因為缺少必要的隔離,我們就沒有辦法利用類加載器去實現容器的邏輯。
- 單一性,由于父加載器的類型對于子加載器是可見的,所以父加載器中加載過的類型,就不會在子加載器中重復加載。但是注意,類加載器“鄰居”間,同一類型仍然可以被加載多次,因為互相并不可見。
類的加載器分類
JVM支持兩種類型的類加載器,分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是Java虛擬機規范卻沒有這么定義,而是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器。無論類加載器的類型如何劃分,在程序中我們最常見的類加載器結構主要是如下情況:
類加載器除了頂層的啟動類加載器外,其余的類加載器都應當有自己的“父類”加載器。
不同類加載器看似是繼承(Inheritance)關系,實際上是包含關系。在下層加載器中,包含著上層加載器的引用。
引導類加載器(Bootstrap ClassLoader)
- 這個類加載使用C/C++語言實現的,嵌套在JVM內部。
- 它用來加載Java的核心庫(JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內容)。用于提供JVM自身需要的類。
- 并不繼承自java.lang.ClassLoader,沒有父加載器。
- 出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類
- 加載擴展類和應用程序類加載器,并指定為他們的父類加載器。
擴展類加載器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現。
- 繼承于ClassLoader類
- 父類加載器為啟動類加載器
- 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載。
代碼示例:
public?static?void?main(String[]?args)?{????//獲取BootstrapcLassLoader能夠加載的api的路徑
????URL[]?urLs?=?sun.misc.Launcher.getBootstrapClassPath().getURLs();
????for?(URL?element?:?urLs)?{
????????System.out.println(element.toExternalForm());
????}
????//????????file:/D:/java/jre/lib/resources.jar
????//????????file:/D:/java/jre/lib/rt.jar
????//????????file:/D:/java/jre/lib/sunrsasign.jar
????//????????file:/D:/java/jre/lib/jsse.jar
????//????????file:/D:/java/jre/lib/jce.jar
????//????????file:/D:/java/jre/lib/charsets.jar
????//????????file:/D:/java/jre/lib/jfr.jar
????//????????file:/D:/java/jre/classes
????//引導類加載
????ClassLoader?classloader?=?java.security.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);
????}
????//????????D:\Java\jre\lib\ext
????//????????C:\Windows\Sun\Java\lib\ext
????//擴展類加載器
????ClassLoader?classLoader1?=?sun.security.ec.CurveDB.class.getClassLoader();
????System.out.println(classLoader1);//?sun.misc.Launcher$ExtClassLoader@6e0be858
}
系統類加載器(AppClassLoader)
- java語言編寫,由sun.misc.Launcher$AppClassLoader實現
- 繼承于ClassLoader類
- 父類加載器為擴展類加載器
- 它負責加載環境變量classpath或系統屬性java.class.path 指定路徑下的類庫
- 應用程序中的類加載器默認是系統類加載器。
- 它是用戶自定義類加載器的默認父加載器
- 通過ClassLoader的getSystemClassLoader()方法可以獲取到該類加載器
用戶自定義類加載器
- 在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的。在必要時,我們還可以自定義類加載器,來定制類的加載方式。
- 體現Java語言強大生命力和巨大魅力的關鍵因素之一便是,Java開發者可以自定義類加載器來實現類庫的動態加載,加載源可以是本地的JAR包,也可以是網絡上的遠程資源。
- 通過類加載器可以實現非常絕妙的插件機制,這方面的實際應用案例舉不勝舉。例如,著名的OSGI組件框架,再如Eclipse的插件機制。類加載器為應用程序提供了一種動態增加新功能的機制,這種機制無須重新打包發布應用程序就能實現。
- 同時,自定義加載器能夠實現應用隔離,例如Tomcat,Spring等中間件和組件框架都在內部實現了自定義的加載器,并通過自定義加載器隔離不同的組件模塊。這種機制比C/C++程序要好太多,想不修改C/C++程序就能為其新增功能,幾乎是不可能的,僅僅一個兼容性便能阻擋住所有美好的設想。
- 自定義類加載器通常需要繼承ClassLoader。
ClassLoader源碼分析
ClassLoader與現有類加載器的關系:
ClassLoader的主要方法
public final ClassLoader getParent() :返回該類加載器的超類加載器
**public Class> loadClass(String name) **:加載名稱為name的類,返回結果為java.lang.Class類的實例。如果找不到類,則返回ClassNotFoundException異常。該方法中的邏輯就是使用雙親委派機制實現的。
protected?Class>?loadClass(String?name,?boolean?resolve)?//resolve?=?false?不進行解析
????????throws?ClassNotFoundException
????{
????????synchronized?(getClassLoadingLock(name))?{
????????????//?First,?check?if?the?class?has?already?been?loaded
????????????//?查看該類是否被加載過
????????????Class>?c?=?findLoadedClass(name);
????????????if?(c?==?null)?{
????????????????long?t0?=?System.nanoTime();
????????????????try?{
????????????????????//獲取當前類父類的加載器
????????????????????if?(parent?!=?null)?{
????????????????????????//使用父類的加載器加載
????????????????????????c?=?parent.loadClass(name,?false);
????????????????????}?else?{
????????????????????????//說明父類加載器為引導類加載器
????????????????????????c?=?findBootstrapClassOrNull(name);
????????????????????}
????????????????}?catch?(ClassNotFoundException?e)?{
????????????????????//?ClassNotFoundException?thrown?if?class?not?found
????????????????????//?from?the?non-null?parent?class?loader
????????????????}
????????????????if?(c?==?null)?{//當前類的加載器父類加載器未加載此類?or?當前類的加載器未加載此類
????????????????????//?If?still?not?found,?then?invoke?findClass?in?order
????????????????????//?to?find?the?class.
????????????????????long?t1?=?System.nanoTime();
????????????????????//調用當前Classloader的findClass
????????????????????c?=?findClass(name);
????????????????????//?this?is?the?defining?class?loader;?record?the?stats
????????????????????sun.misc.PerfCounter.getParentDelegationTime().addTime(t1?-?t0);
????????????????????sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
????????????????????sun.misc.PerfCounter.getFindClasses().increment();
????????????????}
????????????}
????????????if?(resolve)?{?//是否解析
????????????????resolveClass(c);
????????????}
????????????return?c;
????????}
}**protected Class > findClass(String name) **:查找二進制名稱為name的類,返回結果為java.lang.Class類的實例。這是一個受保護的方法,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委托機制,該方法會在檢查完父類加載器之后被loadClass()方法調用。
protected?Class>?findClass(String?name)?throws?ClassNotFoundException?{
????????throw?new?ClassNotFoundException(name);
}我們找到對應重寫該方法的地方。URLClassLoader類。
protected?Class>?findClass(final?String?name)
????????throws?ClassNotFoundException
????{
????????final?Class>?result;
????????try?{
????????????result?=?AccessController.doPrivileged(
????????????????new?PrivilegedExceptionAction>()?{public?Class>?run()?throws?ClassNotFoundException?{//全路徑替換
????????????????????????String?path?=?name.replace('.',?'/').concat(".class");
????????????????????????Resource?res?=?ucp.getResource(path,?false);if?(res?!=?null)?{try?{//調用defineClass生成Class對象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;
}protected final Class>defineclass(String name,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對象
代碼示例:
@Override
protected?Class>findClass(String?name)throws?ClassNotFoundException{
????//獲取類的字節數組
????byte[]classData=getClassData(name);
????if(classData==null){
????????throw?new?ClassNotFoundException();
????}else{
????????//使用defineclass生成class對象
????????return?defineclass(name,classData,0,classData.length);
????}
}protected final void resolveClass(Class>c):鏈接指定的一個Java類。使用該方法可以使用類的Class對象創建完成的同時也被解析。前面我們說鏈接階段主要是對字節碼進行驗證,為類變量分配內存并設置初始值同時將字節碼文件中的符號引用轉換為直接引用。
protected final Class>findLoadedClass(String name):查找名稱為name的已經被加載過的類,返回結果為java.lang.Class類的實例。這個方法是final方法,無法被修改。
**private final ClassLoader parent:**它也是一個ClassLoader的實例,這個字段所表示的ClassLoader也稱為這個ClassLoader的雙親。在類加載的過程中,ClassLoader可能會將某些請求交予自己的雙親處理。
SecureClassLoader 與 URLClassLoader
SecureClassLoader擴展了ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關聯。
前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實現,比如findClass()、findResource()等。而URLClassLoader這個實現類為這些方法提供了具體的實現。并新增了URLClassPath類協助取得Class字節碼流等功能。在編寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
雙親委派模型
如果一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回。只有父類加載器無法完成此加載任務時,才自己去加載。
本質
規定了類加載的順序是:引導類加載器先加載,若加載不到,由擴展類加載器加載,若還加載不到,才會由系統類加載器或自定義的類加載器進行加載。
雙親委派機制類加載流程優勢與劣勢
優勢
- 避免類的重復加載,確保一個類的全局唯一性
- Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重復加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。
- 保護程序安全,防止核心API被隨意篡改
問題
如果在自定義的類加載器中重寫java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadclass(String,boolean)方法,抹去其中的雙親委派機制,那么是不是就能夠加載核心類庫了呢?這也不行!因為JDK還為核心類庫提供了一層保護機制。不管是自定義的類加載器,還是系統類加載器抑或擴展類加載器,最終都必須調用 java.lang.ClassLoader.defineclass(String,byte[],int,int,ProtectionDomain)方法,而該方法會執行preDefineClass()接口,該接口中提供了對JDK核心類庫的保護。
劣勢
- 檢查類是否加載的委托過程是單向的,這個方式雖然從結構上說比較清晰,使各個ClassLoader的職責非常明確,但是同時會帶來一個問題,即頂層的ClassLoader無法訪問底層的ClassLoader所加載的類。
- 通常情況下,啟動類加載器中的類為系統核心類,包括一些重要的系統接口,而在應用類加載器中,為應用類。按照這種模式,應用類訪問系統類自然是沒有問題,但是系統類訪問應用類就會出現問題。比如在系統類中提供了一個接口,該接口需要在應用類中得以實現,該接口還綁定一個工廠方法,用于創建該接口的實例,而接口和工廠方法都在啟動類加載器中。這時,就會出現該工廠方法無法創建由應用類加載器加載的應用實例的問題。
自定義類加載器
為什么要自定義類加載器?
- 隔離加載類
- 在某些框架內進行中間件與應用的模塊隔離,把類加載到不同的環境。比如:阿里內某容器框架通過自定義類加載器確保應用中依賴的jar包不會影響到中間件運行時使用的jar包。再比如:Tomcat這類Web應用服務器,內部自定義了好幾種類加載器,用于隔離同一個Web應用服務器上的不同應用程序。
- 修改類加載的方式
- 類的加載模型并非強制,除Bootstrap外,其他的加載并非一定要引入,或者根據實際情況在某個時間點進行按需進行動態加載
- 擴展加載源
- 比如從數據庫、網絡、甚至是電視機機頂盒進行加載
- 防止源碼泄漏
- Java代碼容易被編譯和篡改,可以進行編譯加密。那么類加載也需要自定義,還原加密的字節碼。
常見的場景
- 實現類似進程內隔離,類加載器實際上用作不同的命名空間,以提供類似容器、模塊化的效果。例如,兩個模塊依賴于某個類庫的不同版本,如果分別被不同的容器加載,就可以互不干擾。這個方面的集大成者是JavaEE和OSGI、JPMS等框架。
- 應用需要從不同的數據源獲取類定義信息,例如網絡數據源,而不是本地文件系統。或者是需要自己操縱字節碼,動態修改或者生成類型。
注意
在一般情況下,使用不同的類加載器去加載不同的功能模塊,會提高應用程序的安全性。但是,如果涉及Java類型轉換,則加載器反而容易產生不美好的事情。在做Java類型轉換時,只有兩個類型都是由同一個加載器所加載,才能進行類型轉換,否則轉換時會發生異常。
實現方式
Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應該繼承ClassLoader類。
在自定義ClassLoader的子類時候,我們常見的會有兩種做法:
- 方式一:重寫loadClass()方法
- 方式二:重寫findclass()方法(推薦)
這兩種方法本質上差不多,畢竟loadClass()也會調用findClass(),但是從邏輯上講我們最好不要直接修改loadClass()的內部邏輯。建議的做法是只在findClass()里重寫自定義類的加載方法,根據參數指定類的名字,返回對應的Class對象的引用。
loadclass()這個方法是實現雙親委派模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此我們最好是在雙親委派模型框架內進行小范圍的改動,不破壞原有的穩定結構。同時,也避免了自己重寫loadClass()方法的過程中必須寫雙親委托的重復代碼,從代碼的復用性來看,不直接修改這個方法始終是比較好的選擇。
當編寫好自定義類加載器后,便可以在程序中調用loadClass()方法來實現類加載操作。
代碼示例:
public?class?MyClassLoad?extends?ClassLoader?{????private?String?byteCodePath;
????public?MyClassLoad(String?byteCodePath)?{
????????this.byteCodePath?=?byteCodePath;
????}
????@Override
????protected?Class>?findClass(String?name)?throws?ClassNotFoundException?{
????????BufferedInputStream?bufferedInputStream?=?null;
????????ByteArrayOutputStream?byteOutputStream?=?null;
????????try?{
????????????//?獲取完整的字節碼文件路徑+class文件名
????????????String?fileName?=?byteCodePath?+?name?+?".class";
????????????//?獲取一個輸入流
????????????bufferedInputStream?=?new?BufferedInputStream(new?FileInputStream(fileName));
????????????//?獲取一個輸出流
????????????byteOutputStream?=?new?ByteArrayOutputStream();
????????????//?具體讀取數據并寫出的過程
????????????int?len;
????????????byte[]?data?=?new?byte[1024];
????????????while?((len?=?bufferedInputStream.read(data))?!=?-1)?{
????????????????byteOutputStream.write(data,?0,?len);
????????????}
????????????//?將輸出流轉成數組
????????????byte[]?byteCode?=?byteOutputStream.toByteArray();
????????????//?調用defineClass(),將字節數組轉成Class實列
????????????Class>?aClass?=?defineClass(null,?byteCode,?0,?byteCode.length);
????????????return?aClass;
????????}?catch?(IOException?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????try?{
????????????????if?(bufferedInputStream?!=?null)?{
????????????????????bufferedInputStream.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????????try?{
????????????????if?(byteOutputStream?!=?null)?{
????????????????????byteOutputStream.close();
????????????????}
????????????}?catch?(IOException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????????return?null;
????}
}
測試類:
public?class?MyClassLoadTest?{????public?static?void?main(String[]?args)?throws?ClassNotFoundException?{
????????MyClassLoad?myClassLoad?=?new?MyClassLoad("D:/");
????????Class>?myClassLoadClass?=?myClassLoad.findClass("Demo1");
????????System.out.println(myClassLoadClass.getClassLoader().getClass().getName());??System.out.println(myClassLoadClass.getClassLoader().getParent().getClass().getName());?
????}
}
Java9新特性
為了保證兼容性,JDK9沒有從根本上改變三層類加載器架構和雙親委派模型,但為了模塊化系統的順利運行,仍然發生了一些值得被注意的變動。
變化
擴展機制被移除,擴展類加載器由于向后兼容性的原因被保留,不過被重命名為平臺類加載器(platform class loader)。可以通過classLoader的新方法getPlatformClassLoader()來獲取。
JDK9時基于模塊化進行構建(原來的rt.jar和tools.jar被拆分成數十個JMOD文件),其中的Java類庫就已天然地滿足了可擴展的需求,那自然無須再保留\lib\ext目錄,此前使用這個目錄或者java.ext.dirs系統變量來擴展JDK功能的機制已經沒有繼續存在的價值了。
平臺類加載器和應用程序類加載器都不再繼承自java.net.URLClassLoader。
現在啟動類加載器、平臺類加載器、應用程序類加載器全都繼承于jdk.internal.loader.BuiltinClassLoader。
總結
以上是生活随笔為你收集整理的加载类_JVM类加载详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 交易猫如何卖号
- 下一篇: log4j 禁止类输出日志_log4j