原文轉自:http://blog.csdn.net/cxhzqhzq/article/details/6686121
1. 從ClassNotFoundException談起
編碼的時候,我們常常可以看到ClassNotFoundException,比如在jdbc連接的時候,引入jar包不完全的時候等,我們一看就知道這個是由於找不到相關類庫導致的,那麼這個是從什麼地方產生的?爲什麼會拋出這個異常呢?這就是本文要解決的問題。
2. 類加載器是什麼
顧名思義,類加載器(class loader)用來加載Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用Java 類的方式如下:Java 源程序(.java 文件)在經過Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class 類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的 newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加複雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。
基本上所有的類加載器都是 java.lang.ClassLoader 類的一個實例。
2.1. ClassLocader類介紹
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 類。 |
類加載器是負責加載類的對象。ClassLoader 類是一個抽象類。如果給定類的二進制名稱,那麼類加載器會試圖查找或生成構成類定義的數據。一般策略是將名稱轉換爲某個文件名,然後從文件系統讀取該名稱的“類文件”。
每個 Class 對象都包含一個對定義它的 ClassLoader 的引用。
數組類的 Class 對象不是由類加載器創建的,而是由 Java 運行時根據需要自動創建。數組類的類加載器由 Class.getClassLoader()返回,該加載器與其元素類型的類加載器是相同的;如果該元素類型是基本類型,則該數組類沒有類加載器。
應用程序需要實現ClassLoader 的子類,以擴展 Java 虛擬機動態加載類的方式。
ClassLoader類使用委託模型來搜索類和資源。每個 ClassLoader 實例都有一個相關的父類加載器。需要查找類或資源時,ClassLoader實例會在試圖親自查找類或資源之前,將搜索類或資源的任務委託給其父類加載器。虛擬機的內置類加載器(稱爲 “bootstrapclass loader”)本身沒有父類加載器,但是可以將它用作 ClassLoader 實例的父類加載器。
3. 分類
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 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。下圖中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
3.1. 類加載過程
在前面介紹類加載器的代理模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味着真正完成類的加載工作的類加載器和啓動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用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
方法不會被重複調用。
3.2. 類加載代理機制
類加載器在嘗試自己去查找某個類的字節代碼並定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推,這種方式稱爲代理模型。
在介紹代理模式背後的動機之前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認爲兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之後所得到的類,也是不同的。比如一個 Java 類com.example.Sample,編譯之後生成了字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA 和 ClassLoaderB 分別讀取了這個 Sample.class 文件,並定義出兩個 java.lang.Class 類的實例來表示這個類。這兩個實例是不相同的。對於 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常ClassCastException。下面通過示例來具體說明。
瞭解了這一點之後,就可以理解代理模式的設計動機了。代理模式是爲了保證 Java 核心庫的類型安全。所有 Java 應用都至少需要引用 java.lang.Object 類,也就是說在運行的時候,java.lang.Object 這個類需要被加載到 Java 虛擬機中。如果這個加載過程由 Java 應用自己的類加載器來完成的話,很可能就存在多個版本的 java.lang.Object類,而且這些類之間是不兼容的。通過代理模式,對於 Java 核心庫的類的加載工作由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
不同的類加載器爲相同名稱的類創建了額外的名稱空間。相同名稱的類可以並存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當於在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到。
我們可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機默認採用的代理模型到底是什麼模樣:
- /**
- * Loads the class with the specified <a href="#name">binary name</a>. The
- * default implementation of this method searches for classes in the
- * following order:
- *
- * <p><ol>
- *
- * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
- * has already been loaded. </p></li>
- *
- * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
- * on the parent class loader. If the parent is <tt>null</tt> the class
- * loader built-in to the virtual machine is used, instead. </p></li>
- *
- * <li><p> Invoke the {@link #findClass(String)} method to find the
- * class. </p></li>
- *
- * </ol>
- *
- * <p> If the class was found using the above steps, and the
- * <tt>resolve</tt> flag is true, this method will then invoke the {@link
- * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
- *
- * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
- * #findClass(String)}, rather than this method. </p>
- *
- * <p> Unless overridden, this method synchronizes on the result of
- * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
- * during the entire class loading process.
- *
- * @param name The <a href="#name">binary name</a> of the class
- *
- * @param resolve If <tt>true</tt> then resolve the class
- *
- * @return The resulting <tt>Class</tt> object
- *
- * @throws ClassNotFoundException If the class could not be found
- */
- protected Class<?> loadClass(String name, boolean resolve)
- 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 {
- ////如果不存在父類加載器,就檢查是否是由啓動類加載器加載的類,
- //通過調用本地方法native Class findBootstrapClass(String name)
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- // // 如果父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能
- long t1 = System.nanoTime();
- 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;
- }
- }
通過上面對於源代碼的分析 ,相比大家都已近對代理模型有所瞭解了吧。
4. java程序動態擴展方式
Java的連接模型允許用戶運行時擴展引用程序,既可以通過當前虛擬機中預定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時並不知道或者尚未存在的類或者接口,並動態連接它們並進行有選擇的解析。
運行時動態擴展java應用程序有調用Class.forName方法和自定義類加載器兩種方法。
4.1. 調用java.lang.Class.forName(…)
- public static Class<?> forName(String className)
- public static Class<?> forName(String name, boolean initialize,
- ClassLoader loader)
Class.forName 是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(Stringname, 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 數據庫的驅動。
說明:
在一個實例方法中,表達式:
Class.forName("Foo")
等效於:
Class.forName("Foo", true, this.getClass().getClassLoader())
4.2. 用戶自定義類加載器
一般用戶自定義類加載器的工作流程如下:
1、首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則轉入步驟2
2、委派類加載請求給父類加載器(更準確的說應該是雙親類加載器,真個虛擬機中各種類加載器最終會呈現樹狀結構),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉入步驟3
3、調用本類加載器的findClass(…)方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass(…)導入類型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass(…), loadClass(…)轉拋異常,終止加載過程(注意:這裏的異常種類不止一種)。
下面是一個自己寫的類加載器的示例:
- public class FileSystemClassLoader extends ClassLoader {
- private String rootDir;
- public FileSystemClassLoader(String rootDir) {
- this.rootDir = rootDir;
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- }
- else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead = 0;
- while ((bytesNumRead = ins.read(buffer)) != -1) {
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- }
- }
類 FileSystemClassLoader 繼承自類java.lang.ClassLoader,一般來說,自己開發的類加載器只需要覆寫findClass(String name) 方法即可。java.lang.ClassLoader 類的方法 loadClass() 封裝了前面提到的代理模式的實現。該方法會首先調用findLoadedClass() 方法來檢查該類是否已經被加載過;如果沒有加載過的話,會調用父類加載器的loadClass() 方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調用 findClass() 方法來查找該類。因此,爲了保證類加載器都正確實現代理模式,在開發自己的類加載器時,最好不要覆寫 loadClass() 方法,而是覆寫 findClass() 方法。
類 FileSystemClassLoader 的 findClass() 方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些字節代碼轉換成 java.lang.Class 類的實例。
5. 預先加載與依需求加載
Java 運行環境爲了優化系統,提高程序的執行速度,在 JRE 運行的開始會將 Java 運行所需要的基本類採用預先加載( pre-loading )的方法全部加載要內存當中,因爲這些單元在 Java 程序運行的過程當中經常要使用的,主要包括 JRE 的 rt.jar 文件裏面所有的 .class 文件。
當 java.exe 虛擬機開始運行以後,它會找到安裝在機器上的 JRE 環境,然後把控制權交給 JRE , JRE 的類加載器會將 lib 目錄下的 rt.jar 基礎類別文件庫加載進內存,這些文件是 Java 程序執行所必須的,所以系統在開始就將這些文件加載,避免以後的多次 IO 操作,從而提高程序執行效率。
相對於預先加載,我們在程序中需要使用自己定義的類的時候就要使用依需求加載方法( load-on-demand ),就是在 Java 程序需要用到的時候再加載,以減少內存的消耗,因爲 Java 語言的設計初衷就是面向嵌入式領域的。
在這裏還有一點需要說明的是, JRE 的依需求加載究竟是在什麼時候把類加載進入內部的呢?
我們在定義一個類實例的時候,比如 TestClassAtestClassA ,這個時候 testClassA 的值爲 null ,也就是說還沒有初始化,沒有調用 TestClassA 的構造函數,只有當執行 testClassA= new TestClassA() 以後, JRE 才正真把 TestClassA 加載進來。
6. 隱式加載和顯示加載
Java 的加載方式分爲隱式加載( implicit )和顯示加載( explicit )。所謂隱式加載就是我們在程序中用 new 關鍵字來定義一個實例變量, JRE 在執行到 new 關鍵字的時候就會把對應的實例類加載進入內存。隱式加載的方法很常見,用的也很多, JRE 系統在後臺自動的幫助用戶加載,減少了用戶的工作量,也增加了系統的安全性和程序的可讀性。
相對於隱式加載的就是我們不經常用到的顯示加載。所謂顯示加載就是有程序員自己寫程序把需要的類加載到內存當中,下面我們看一段程序:- package com.mytestcodes.classloader;
- class TestClass
- {
- public void method()
- {
- System.out.println("TestClass-method");
- }
- }
- public class CLTest
- {
- public static void main(String args[])
- {
- try
- {
- Class<?> c = Class.forName("com.mytestcodes.classloader.TestClass");
- TestClass object = (TestClass) c.newInstance();
- object.method();
- } catch (Exception e)
- {
- e.printStackTrace();
- }
- }
- }
7. 類加載器與OSGI
OSGi™是 Java 上的動態模塊系統。它爲開發人員提供了面向服務和基於組件的運行環境,並提供標準的方式用來管理軟件的生命週期。OSGi 已經被實現和部署在很多產品上,在開源社區也得到了廣泛的支持。Eclipse 就是基於 OSGi 技術來構建的。
OSGi中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java 開頭的包和類),它會代理給父類加載器(通常是啓動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統屬性org.osgi.framework.bootdelegation 的值即可。
假設有兩個模塊bundleA 和bundleB,它們都有自己對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,並且該類被聲明爲導出的,也就是說可以被其它模塊所使用的。bundleB 聲明瞭導入 bundleA 提供的類com.bundleA.Sample,幷包含一個類com.bundleB.NewSample 繼承自 com.bundleA.Sample。在 bundleB 啓動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進而需要加載類com.bundleA.Sample。由於bundleB 聲明瞭類com.bundleA.Sample 是導入的,classLoaderB 把加載類 com.bundleA.Sample 的工作代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內部查找類com.bundleA.Sample 並定義它,所得到的類 com.bundleA.Sample 實例就可以被所有聲明導入了此類的模塊使用。對於以 java 開頭的類,都是由父類加載器來加載的。如果聲明瞭系統屬性org.osgi.framework.bootdelegation=com.example.core.*,那麼對於包 com.example.core中的類,都是由父類加載器來完成的。
OSGi模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。
如果一個類庫被多個模塊共用,可以爲這個類庫單獨的創建一個模塊,把其它模塊需要用到的 Java 包聲明爲導出的。其它模塊聲明導入這些類。
如果類庫提供了 SPI 接口,並且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了NoClassDefFoundError 異常,首先檢查當前線程的上下文類加載器是否正確。通過Thread.currentThread().getContextClassLoader() 就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader()來得到模塊對應的類加載器,再通過Thread.currentThread().setContextClassLoader() 來設置當前線程的上下文類加載器。