深入探讨 Java 类加载器(一)
2019獨角獸企業重金招聘Python工程師標準>>>
類加載器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態加載到 Java 虛擬機中并執行。類加載器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠程下載 Java 類文件到瀏覽器中并執行。現在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類加載器進行交互。Java 虛擬機默認的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類加載器進行交互的情況,而對類加載器的機制又不是很了解的話,就很容易花大量 的時間去調試 ClassNotFoundException和 NoClassDefFoundError等異常。本文將詳細介紹 Java 的類加載器,幫助讀者深刻理解 Java 語言中的這個重要概念。下面首先介紹一些相關的基本概念。
類加載器基本概念
顧 名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之后就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,并轉換成 java.lang.Class類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加復雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。
基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例。下面詳細介紹這個 Java 類。
java.lang.ClassLoader類介紹
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然后從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。不過本文只討論其加載類的功能。為了完成加載類的這個職責,ClassLoader提供了一系列的方法,比較重要的方法如 表 1所示。關于這些方法的細節會在下面進行介紹。
表 1. 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 類。 |
對于 表 1中給出的方法,表示類名稱的 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類的方式實現自己的類加載器,以滿足一些特殊的需求。
除了引導類加載器之外,所有的類加載器都有一個父類加載器。通過 表 1中給出的 getParent()方 法可以得到。對于系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對于開發人員編寫的類加 載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根 節點就是引導類加載器。圖 1中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
圖 1. 類加載器樹狀組織結構示意圖
代碼清單 1演示了類加載器的樹狀組織結構。
清單 1. 演示類加載器的樹狀組織結構
?public?class?ClassLoaderTree?{?public?static?void?main(String[]?args)?{?ClassLoader?loader?=?ClassLoaderTree.class.getClassLoader();?while?(loader?!=?null)?{?System.out.println(loader.toString());?loader?=?loader.getParent();?}?}?}每個 Java 類都維護著一個指向定義它的類加載器的引用,通過 getClassLoader()方法就可以獲取到此引用。代碼清單 1中通過遞歸調用 getParent()方法來輸出全部的父類加載器。代碼清單 1的運行結果如 代碼清單 2所示。
清單 2. 演示類加載器的樹狀組織結構的運行結果
?sun.misc.Launcher$AppClassLoader@9304b1?sun.misc.Launcher$ExtClassLoader@190d11如 代碼清單 2所示,第一個輸出的是 ClassLoaderTree類的類加載器,即系統類加載器。它是 sun.misc.Launcher$AppClassLoader類的實例;第二個輸出的是擴展類加載器,是 sun.misc.Launcher$ExtClassLoader類的實例。需要注意的是這里并沒有輸出引導類加載器,這是由于有些 JDK 的實現對于父類加載器是引導類加載器的情況,getParent()方法返回 null。
在了解了類加載器的樹狀組織結構之后,下面介紹類加載器的代理模式。
類加載器的代理模式
類 加載器在嘗試自己去查找某個類的字節代碼并定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推。在介紹代理模式背后的動機之 前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的字節代碼,被不同的類加 載器加載之后所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之后生成了字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件,并定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的。對于 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。下面通過示例來具體說明。代碼清單 3中給出了 Java 類 com.example.Sample。
清單 3. com.example.Sample 類
?package?com.example;?public?class?Sample?{?private?Sample?instance;?public?void?setSample(Object?instance)?{?this.instance?=?(Sample)?instance;?}?}如 代碼清單 3所示,com.example.Sample類的方法 setSample接受一個 java.lang.Object類型的參數,并且會把該參數強制轉換成 com.example.Sample類型。測試 Java 類是否相同的代碼如 代碼清單 4所示。
清單 4. 測試 Java 類是否相同
?public?void?testClassIdentity()?{?String?classDataRootPath?=?"C:\\workspace\\Classloader\\classData";?FileSystemClassLoader?fscl1?=?new?FileSystemClassLoader(classDataRootPath);?FileSystemClassLoader?fscl2?=?new?FileSystemClassLoader(classDataRootPath);?String?className?=?"com.example.Sample";? try?{?Class<?>?class1?=?fscl1.loadClass(className);?Object?obj1?=?class1.newInstance();?Class<?>?class2?=?fscl2.loadClass(className);?Object?obj2?=?class2.newInstance();?Method?setSampleMethod?=?class1.getMethod("setSample",?java.lang.Object.class);?setSampleMethod.invoke(obj1,?obj2);?}?catch?(Exception?e)?{?e.printStackTrace();?}?}代碼清單 4中使用了類 FileSystemClassLoader的兩個不同實例來分別加載類 com.example.Sample,得到了兩個不同的 java.lang.Class的實例,接著通過 newInstance()方法分別生成了兩個類的對象 obj1和 obj2,最后通過 Java 的反射 API 在對象 obj1上調用方法 setSample,試圖把對象 obj2賦值給 obj1內部的 instance對象。代碼清單 4的運行結果如 代碼清單 5所示。
清單 5. 測試 Java 類是否相同的運行結果
java.lang.reflect.InvocationTargetException? at?sun.reflect.NativeMethodAccessorImpl.invoke0(Native?Method)? at?sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)? at?sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at?java.lang.reflect.Method.invoke(Method.java:597)? at?classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)? at?classloader.ClassIdentity.main(ClassIdentity.java:9)? Caused?by:?java.lang.ClassCastException:?com.example.Sample? cannot?be?cast?to?com.example.Sample? at?com.example.Sample.setSample(Sample.java:7)? ...?6?more從 代碼清單 5給出的運行結果可以看到,運行時拋出了 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 類空間。這種技術在許多框架中都被用到,后面會詳細介紹。
下面具體介紹類加載器加載類的詳細過程。
加載類的過程
在前面介紹類加載器的代理模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用 defineClass來實現的;而啟動類的加載過程是通過調用 loadClass來 實現的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器。兩種類加 載器的關聯之處在于:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義加載器負責啟動類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個類之后,會把得到的 java.lang.Class類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對于一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調用。
下面討論另外一種類加載器:線程上下文類加載器。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 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 的實現中都會用到。
下面介紹另外一種加載類的方法:Class.forName。
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 數據庫的驅動。
在介紹完類加載器相關的基本概念之后,下面介紹如何開發自己的類加載器。
轉載于:https://my.oschina.net/kakakaka/blog/337091
總結
以上是生活随笔為你收集整理的深入探讨 Java 类加载器(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《无畏契约》体验:萌新体验不错 FPS+
- 下一篇: AGM PAD P1 三防安卓平板即将推