Java类加载机制深度分析
為什么80%的碼農都做不了架構師?>>> ??
Java類加載機制
類加載是Java程序運行的第一步,研究類的加載有助于了解JVM執行過程,并指導開發者采取更有效的措施配合程序執行。研究類加載機制的第二個目的是讓程序能動態的控制類加載,比如熱部署等,提高程序的靈活性和適應性。
??
在java.lang包里有個ClassLoader類,ClassLoader 的基本目標是對類的請求提供服務,按需動態裝載類和資源,只有當一個類要使用(使用new 關鍵字來實例化一個類)的時候,類加載器才會加載這個類并初始化。一個Java應用程序可以使用不同類型的類加載器。例如Web Application Server中,Servlet的加載使用開發商自定義的類加載器, java.lang.String在使用JVM系統加載器。
在JVM里由類名和類加載器區別不同的Java類型。因此,JVM允許我們使用不同的加載器加載相同namespace的java類,而實際上這些相同namespace的java類可以是完全不同的類。這種機制可以保證JDK自帶的java.lang.String是唯一的。
一、Java類加載器
??
顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,并轉換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加復雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例。
??
二、java.class.ClassLoader類介紹
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然后從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。
??
ClassLoader 中與加載類相關的方法:
getParent()?????????? 返回該類加載器的父類加載器。
loadClass(String name)????? 加載名稱為 name的類,返回的結果是 java.lang.Class類的實例。
findClass(String name)????? 查找名稱為 name的類,返回的結果是 java.lang.Class類的實例。
findLoadedClass(String name)?? 查找名稱為 name的已經被加載過的類,返回的結果是 java.lang.Class類的實例。
defineClass(String name, byte[] b, int off, int len)?? 把字節數組 b中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的實例。這個方法被聲明為 final的。
resolveClass(Class<?> c)???? 鏈接指定的 Java 類。
對于以上給出的方法,表示類名稱的 name參數的值是類的二進制名稱。需要注意的是內部類的表示,如 com.example.Sample$1和com.example.Sample$Inner等表示方式。
??
三、類加載器的樹狀組織結構
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
?引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,并不繼承自 java.lang.ClassLoader。
?擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載Java 類。
?系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。除了引導類加載器之外,所有的類加載器都有一個父類加載器。通過給出的 getParent()方法可以得到。對于系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對于開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。
如以上三圖給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
?
如上圖給出了類加載器的樹狀組織結構演示代碼。
??
四、類加載器的代理模式
??
類加載器在嘗試自己去查找某個類的字節代碼并定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載(loadClass)這個類,依次類推。在介紹代理模式背后的動機之前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之后所得到的類,也是不同的。
??
比如:一個 Java 類 com.example.Sample,編譯之后生成了字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件,并定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的。對于 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。??
?
如上圖代碼,但未出現上述的異常,因為FileSystemClassLoader已繼承自java.class.ClassLoader類,從而使用了類加載的代理機制,兩個類加載過程都是由父類加器完成的,所以不會拋出ClassCastException異常;此處先暫時假定會拋出ClassCastException異常;
有關ClassLoader還有很重要一點:
同一個ClassLoader加載的類文件,只有一個Class實例。但是,如果同一個類文件被不同的ClassLoader載入,則會有兩份不同的ClassLoader實例(前提是著兩個類加載器不能用相同的父類加載器)。
??
運行結果可以看到,運行時拋出了 java.lang.ClassCastException異常。雖然兩個對象 obj1和 obj2的類的名字相同,但是這兩個類是由不同的類加載器實例來加載的,因此不被 Java 虛擬機認為是相同的。了解了這一點之后,就可以理解代理模式的設計動機了。代理模式是為了保證 Java 核心庫的類型安全。所有Java 應用都至少需要引用 java.lang.Object類,也就是說在運行的時候,java.lang.Object這個類需要被加載到 Java 虛擬機中。如果這個加載過程由 Java 應用自己的類加載器來完成的話,很可能就存在多個版本的 java.lang.Object類,而且這些類之間是不兼容的。通過代理模式,對于 Java 核心庫的類的加載工作由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。不同的類加載器為相同名稱的類創建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當于在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間。
??
五、加載類的過程
??
在前面介紹類加載器的代理模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載(loadClass)某個類。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用 defineClass來實現的;而啟動類的加載過程是通過調用 loadClass來實現的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器(此句可與第四部分一起理解哦)。也就是說,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器。
兩種類加載器的關聯之處在于:一個類的定義加載器是它引用的其它類的初始加載器。(有點晦澀,這句話可以舉例說明:ClassA的類加載器為ClassLoaderA,ClassLoaderB是ClassLoaderA父類加載器,那么當ClassLoaderA初始加載ClassA時,由于類加載器的代理模式,則會調用父類加載器ClassLoaderB來定義ClassA,所以ClassLoaderA叫做ClassA的初始加載器,而ClassLoaderB叫做ClassA的定義加載器,然而ClassA中引用了ClassC,那么當父類加載器ClassLoaderB定義ClassLoaderA時,會初始加載ClassC,所以ClassLoaderB又叫做ClassC的初始加載器,又由于類加載器的代理模式,則會調用ClassLoaderB的父類加載器--系統類加載器來定義ClassC,所以最后系統類加載器叫作ClassC的定義加載器)。
如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啟動類 com.example.Inner的加載過程。
??
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對于一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調用(可借類加載這一特點,實現線程安全哦)。
一般簡單加載過程:
Java程序運行的場所是內存,當在命令行下執行:java HelloWorld命令的時候,JVM會將HelloWorld.class加載到內存中,并形成一個Class的對象HelloWorld.class。
其中的過程就是類加載過程:
1、尋找jre目錄,尋找jvm.dll,并初始化JVM;
2、產生一個Bootstrap Loader(引導類加載器);
3、Bootstrap Loader自動加載Extended Loader(擴展類加載器),并將其父Loader設為Bootstrap Loader。
4、Bootstrap Loader自動加載AppClass Loader(系統類加載器),并將其父Loader設為Extended Loader。
5、最后由AppClass Loader加載HelloWorld類。
?
如圖,給出了類加載器各自搜索目錄代碼。
類加載器特點:
1、運行一個程序時,總是由AppClass Loader(系統類加載器)開始加載指定的類。
2、在加載類時,每個類加載器會將加載任務上交給其父,如果其父找不到,再由自己去加載。
3、Bootstrap Loader(引導類加載器)是最頂級的類加載器了,其父加載器為null。
六、線程上下文類加載器
類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。
在線程中運行的代碼可以通過此類加載器來加載類和資源。
??
前面提到的類加載器的代理模式并不能解決 Java 應用開發中會遇到的類加載器的全部問題。如:Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實例。這里的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在于,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
??
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。此處理解很晦澀,簡單說明下個人理解:
1. 首先說明類加載器代理機制解決不了的問題:引導類加載器僅會加載Java核心庫,如SPI接口,但是對于第三方對SPI接口實現的Java類,引導類加載器卻無法加載(引導類加載器是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫),它也不能代理給系統類加載器,因為它是系統類加載器的祖先類加載器。所以導致代理機制解決不了該問題。但是SPI 實現的 Java 類一般會被系統類加載器來加載(通過類路徑CLASSPATH找到),雖然加載了SPI 實現的 Java 類,但導致了SPI接口被引導類加載器加載,SPI接口實現的Java類被系統類加載器加載的困境。
2. 那么面對該問題,使用線程上下文類加載器可解決,因為Java 應用的線程的上下文類加載器默認就是系統上下文類加載器,在 SPI 接口的代碼中使用線程上下文類加載器(即引導類加載器),就可以成功的加載到 SPI 實現的類。從而引導類加載器將SPI接口與SPI實現的Java類一同加載了。該問題解決。
??
七、另外一種加載類的方法:Class.forName
Class.forName是一個靜態方法,同樣可以用來加載類。
該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。
第一種形式的參數 name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。
第二種形式則相當于設置了參數 initialize的值為 true,loader的值為當前類的類加載器(只要不是自定義的類加載器,便是系統類加載)。
Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數據庫的驅動。
??
調用只有一個參數的forName()方法等效于 Class.forName(className, true, loader)。這兩個方法,最后都要連接到原生方法forName0(),其定義如下:
private static native Class forName0(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException;
只有一個參數的forName()方法,最后調用的是:forName0(className, true, ClassLoader.getCallerClassLoader());而三個參數的forName(),最后調用的是:forName0(name, initialize, loader);
所以,不管使用的是new 來實例化某個類、或是使用只有一個參數的Class.forName()方法,內部都隱含了“載入類 + 運行靜態代碼塊”的步驟。而使用具有三個參數的Class.forName()方法時,如果第二個參數為false,那么類加載器只會加載類,而不會初始化靜態代碼塊,只有當實例化這個類的時候,靜態代碼塊才會被初始化;如果第二個參數為true,那么類加載器加載類同時,會初始化靜態代碼塊。
注:類加載有三種方式:不同方式加載時,會影響靜態代碼塊執行順序。
1、命令行啟動應用時候由JVM初始化加載。
2、通過Class.forName()方法動態加載。
3、通過ClassLoader.loadClass()方法動態加載。
?
如圖,給出了各種加載方式與靜態塊執行順序測試類代碼。
八、開發自定義類加載
在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。在某些情況下,您還是需要為應用開發出自己的類加載器。比如您的應用通過網絡來傳輸 Java 類的字節代碼,為了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密后的字節代碼,接著進行解密和驗證,最后定義出要在 Java 虛擬機中運行的類來。
文件系統類加載器:
?
網絡類加載器:
? ?
注:大家在看此處時,不要真的以為是由兩個自定義加載器加載的類文件,有很大誤導,此處是由父類加載器加載完成的,因為加載的類文件與父類加載器是處在同一個project中,導致子類加載器代理給父類加載器時,父類加載器是可以加載到類文件的,從而不會再調用子類加載器的findClass()方法,大家可調試得出結論。我們在使用自定義加載器類時,首先明確的前提是,被加載的類文件,不是當前project產生的,或者是由網絡得來,或者是其他本地類文件,否則會產生以上誤區。
??
九、類加載器與 Web 容器
對于運行在 Java EE?容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規范中的推薦做法,其目的是使得 Web 應用自己的類的優先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內的。這也是為了保證 Java 核心庫的類型安全。
??
絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
?每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
?多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
?當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
十、總結
??
類加載器是 Java 語言的一個創新。它使得動態安裝和更新軟件組件成為可能。本文詳細介紹了類加載器的相關話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器的關系等。開發人員在遇到 ClassNotFoundException和 NoClassDefFoundError等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要注意與已有的類加載器組織結構的協調。
GIT@OSC工程路徑:http://git.oschina.net/taomk/king-training/tree/master/class-loader
轉載于:https://my.oschina.net/xianggao/blog/70826
總結
以上是生活随笔為你收集整理的Java类加载机制深度分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: zabbix2.0安装与配置
- 下一篇: 如何搭建一个指标体系