關於類加載器


寫在前面

每個java開發人員對java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背后就涉及到了java技術體系中的類加載。Java的類加載機制是java技術體系中比較核心的部分,雖然平時沒又怎么接觸,雖然和大部分開發人員直接打交道不多,但是對其背后的機理有一定理解有助於排查程序中出現的類加載失敗等技術問題,對理解java虛擬機的連接模型和java語言的動態性都有很大幫助。

類加載器作用

類加載器(ClassLoader)用來加載 class字節碼到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源文件在經過 Javac之后就被轉換成 Java 字節碼文件(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class 類的一個實例。每一個這樣的實例用來表示一個 Java 類。
類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬中的唯一性。說通俗一些,比較兩個類是否“相等”,只有在兩個類是由同一個類加載器的前提之下才有意義,否則,即使這兩個類來源於同一個class文件,只要加載它的類加載器不同,那這兩個類必定不相等。這里所指的“相等”包括代表類的Class對象的equal方法、isAssignableFrom()、isInstance()方法及instance關鍵字返回的結果。
在框架中需要手動獲取某一個類的實例的時候,最先需要獲取加載到內存的類,然后通過反射來實例化對象就行了。當然現在的框架都已經幫我們把這事做了,比如Spring框架中的org.springframework.util.ClassUtils封裝了自己的類加載器。
當我們需要自己實現一個框架或者需要需要寫一個類加載器的時候,了解類加載器就非常重要了。

Java虛擬機類加載器結構

  1. Bootstrap ClassLoader/啟動類加載器
    主要負責jdk_home/lib目錄下的核心 api 或 -Xbootclasspath 選項指定的jar包裝入工作。
  2. Extension ClassLoader/擴展類加載器
    主要負責jdk_home/lib/ext目錄下的jar包或 -Djava.ext.dirs 指定目錄下的jar包裝入工作。
  3. System ClassLoader/系統類加載器
    主要負責java -classpath/-Djava.class.path所指的目錄下的類與jar包裝入工作。
  4. User Custom ClassLoader/用戶自定義類加載器(java.lang.ClassLoader的子類)
    在程序運行期間, 通過java.lang.ClassLoader的子類動態加載class文件, 體現java動態實時類裝入特性。

類加載器的特性

  1. 每個ClassLoader都維護了一份自己的名稱空間, 同一個名稱空間里不能出現兩個同名的類。
  2. 為了實現java安全沙箱模型頂層的類加載器安全機制, java默認采用了 " 雙親委派的加載鏈 " 結構。
  • 關於類加載雙親委派機制
    如果一個類加載器收到了一個類加載請求,它首先不會自己去加載這個類,而是把這個請求委托給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成加載請求(它管理的范圍之中沒有這個類)時,子加載器才會嘗試着自己去加載。

  • 雙親委派機制的好處

  1. 避免重復加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
  2. Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系,例如java.lang.Object存放在rt.jar之中,無論那個類加載器要加載這個類,最終都是委托給啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類,相反,如果沒有雙親委托模型,由各個類加載器去完成的話,如果用戶自己寫一個名為java.lang.Object的類,並放在classpath中,應用程序中可能會出現多個不同的Object類,java類型體系中最基本安全行為也就無法保證。
  • 類加載器的幾個重要方法
    類加載器均是繼承自java.lang.ClassLoader抽象類。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:
    loadClass: 此方法負責加載指定名字的類,首先會從已加載的類中去尋找,如果沒有找到;從parent ClassLoader[ExtClassLoader]中加載;如果沒有加載到,則從Bootstrap ClassLoader中嘗試加載(findBootstrapClassOrNull方法), 如果還是加載失敗,則拋出異常ClassNotFoundException, 在調用自己的findClass方法進行加載。如果要改變類的加載順序可以覆蓋此方法;如果加載順序相同,則可以通過覆蓋findClass方法來做特殊處理,例如:解密,固定路徑尋找等。當通過整個尋找類的過程仍然未獲取Class對象,則拋出ClassNotFoundException異常。
    如果類需要resolve,在調用resolveClass進行鏈接。
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        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) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

findLoadedClass:此方法負責從當前ClassLoader實例對象的緩存中尋找已加載的類,調用的為native方法。

protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
    return null;
return findLoadedClass0(name);
}
private native final Class findLoadedClass0(String name);

findClass: 此方法直接拋出ClassNotFoundException異常,因此要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

findSystemClass: 此方法是從sun.misc.Launcher$AppClassLoader中尋找類,如果未找到,則繼續從BootstrapClassLoader中尋找,如果仍然未找到,返回null

protected final Class<?> findSystemClass(String name)throws ClassNotFoundException
    {
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        if (!checkName(name))
        throw new ClassNotFoundException(name);
            Class cls = findBootstrapClass(name);
            if (cls == null) {
                throw new ClassNotFoundException(name);
            } 
        return cls;
    }
    return system.loadClass(name);
}

defineClass: 此方法負責將二進制字節流轉換為Class對象,這個方法對於自定義類加載器而言非常重要。如果二進制的字節碼的格式不符合jvm class文件格式規范,則拋出ClassFormatError異常;如果生成的類名和二進制字節碼不同,則拋出NoClassDefFoundError;如果加載的class是受保護的、采用不同簽名的,或者類名是以java.開頭的,則拋出SecurityException異常。

protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)throws ClassFormatError
{
     return defineClassCond(name, b, off, len, protectionDomain, true);
}

// Private method w/ an extra argument for skipping class verification
private final Class<?> defineClassCond(String name,byte[] b, int off, int len,ProtectionDomain protectionDomain,boolean verify)throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);

    Class c = null;
        String source = defineClassSourceLocation(protectionDomain);

    try {
        c = defineClass1(name, b, off, len, protectionDomain, source,verify);
    } catch (ClassFormatError cfe) {
        c = defineTransformedClass(name, b, off, len, protectionDomain, cfe,source, verify);
    }

    postDefineClass(c, protectionDomain);
    return c;
}

resolveClass: 此方法負責完成Class對象的鏈接,如果鏈接過,則直接返回。

線程上下文加載器

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 的實現中都會用到。
java默認的線程上下文類加載器是 系統類加載器(AppClassLoader)。

// Now create the class loader to use to launch the application  
try {  
    loader = AppClassLoader.getAppClassLoader(extcl);  
} catch (IOException e) {  
    throw new InternalError(  
"Could not create application class loader" );  
}   
  
// Also set the context class loader for the primordial thread.  
Thread.currentThread().setContextClassLoader(loader);  

以上代碼摘自sun.misc.Launch的無參構造函數Launch()。
使用線程上下文類加載器, 可以在執行線程中, 拋棄雙親委派加載鏈模式, 使用線程上下文里的類加載器加載類.
典型的例子有, 通過線程上下文來加載第三方庫jndi實現, 而不依賴於雙親委派.
大部分java app服務器(jboss, tomcat..)也是采用contextClassLoader來處理web服務。
還有一些采用 hotswap 特性的框架, 也使用了線程上下文類加載器, 比如 seasar (full stack framework in japenese).
線程上下文從根本解決了一般應用不能違背雙親委派模式的問題.
使java類加載體系顯得更靈活.
隨着多核時代的來臨, 相信多線程開發將會越來越多地進入程序員的實際編碼過程中. 因此,
在編寫基礎設施時, 通過使用線程上下文來加載類, 應該是一個很好的選擇。
當然, 好東西都有利弊. 使用線程上下文加載類, 也要注意, 保證多根需要通信的線程間的類加載器應該是同一個,
防止因為不同的類加載器, 導致類型轉換異常(ClassCastException)。
在黃勇的《架構探險》一書中開發類加載器的時候采用的也是線程上下文加載器。

java動態載入class的兩種方式

  1. implicit隱式,即利用實例化才載入的特性來動態載入class
  2. explicit顯式方式,又分兩種方式:
  • java.lang.Class的forName()方法

  • java.lang.ClassLoader的loadClass()方法

  • 用Class.forName加載類
    Class.forName使用的是被調用者的類加載器來加載類的。
    這種特性, 證明了java類加載器中的名稱空間是唯一的, 不會相互干擾。
    即在一般情況下, 保證同一個類中所關聯的其他類都是由當前類的類加載器所加載的。

public static Class forName(String className)  
     throws ClassNotFoundException {  
     return forName0(className, true , ClassLoader.getCallerClassLoader());  
}   
  
/** Called after security checks have been made. */  
private static native Class forName0(String name, boolean initialize,  
ClassLoader loader) throws ClassNotFoundException; 

上面中 ClassLoader.getCallerClassLoader 就是得到調用當前forName方法的類的類加載器。
上面forName中的initialize參數是很重要的,可以覺得被加載同時是否完成初始化的工作(說明: 單參數版本的forName方法默認是不完成初始化的).有些場景下,需要將initialize設置為true來強制加載同時完成初始化,例如典型的就是利用DriverManager進行JDBC驅動程序類注冊的問題,因為每一個JDBC驅動程序類的靜態初始化方法都用DriverManager注冊驅動程序,這樣才能被應用程序使用,這就要求驅動程序類必須被初始化,而不單單被加載.
有時候為了提高加載類的性能,可以講initialize參數設置為false。

常見異常

  • ClassNotFoundException 這是最常見的異常,產生這個異常的原因為在當前的ClassLoader 中加載類時,未找到類文件,
  • NoClassDefFoundError 這個異常是因為 加載到的類中引用到的另外類不存在,例如要加載A,而A中盜用了B,B不存在或當前的ClassLoader無法加載B,就會拋出這個異常。
  • LinkageError 該異常在自定義ClassLoader的情況下更容易出現,主要原因是此類已經在ClassLoader加載過了,重復的加載會造成該異常。

參考
Java虛擬機學習 - 類加載器(ClassLoader)
java classLoader體系結構使用詳解
Java類加載原理解析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM