java類加載器不完整分析

雖然之前也看過jvm相關的書籍,但是都是概念層次上的理解。今天特地花一天時間研究了下類加載器,感覺上是沒有那麼生疏了,但也只是冰山一角,索性就不完整地分析一番吧。內容有些長,可使用目錄快速查閱。

類加載器

  簡單說下JVM預定義的三種類型的類加載器,這個也算是老生常談了。當JVM啓動一個項目的時候,它將缺省使用以下三種類型的類加載器:
1. 啓動(Bootstrap)類加載器:負責裝載<Java_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包。由native方法實現加載過程,程序無法直接獲取到該類加載器,無法對其進行任何操作。
2. 擴展(Extension)類加載器:擴展類加載器由sun.misc.Launcher.ExtClassLoader實現的。負責加載<Java_Home>/lib/ext或者由系統變量-Djava.ext.dir指定位置中的類庫。程序可以訪問並使用擴展類加載器。
3. 系統(System)類加載器:系統類加載器是由sun.misc.Launcher.AppClassLoader實現的,也叫應用程序類加載器。負責加載系統類路徑-classpath-Djava.class.path變量所指的目錄下的類庫。程序可以訪問並使用系統類加載器。

雙親委派類加載機制

類加載器的父子關係

三種類加載器的父子關係如圖所示
類加載父子關係

注意這兒的父子並不是繼承的意思,它們都是ClassLoader抽象類的實現,因此都含有一個ClassLoader parent成員變量,該變量指向其父加載器,類似單向鏈表。

雙親委派源碼實現

委派關係也被稱爲代理,我們來看看代碼,loadClass是抽象類ClassLoader中的類加載的核心方法。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 若本加載器之前是否已加載過,直接取緩存,native方法實現
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    // 只要有父加載器就先委派父加載器來加載
                    if (parent != null) {
                        // 注意此處遞歸調用
                        c = parent.loadClass(name, false);
                    } else {
                        // ext的parent爲null,因爲Bootstrap是無法被程序被訪問的,默認parent爲null時其父加載器就是Bootstrap
                        // 此時直接用native方法調用啓動類加載加載,若找不到則拋異常
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 對ClassNotFoundException不做處理,僅用作退出遞歸
                }

                if (c == null) {
                    // 如果父加載器無法加載那麼就在本類加載器的範圍內進行查找
                    // findClass找到class文件後將調用defineClass方法把字節碼導入方法區,同時緩存結果
                    c = findClass(name);
                }
            }
            // 是否解析,默認false
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看出所謂的雙親委派的本質就是這兩句遞歸代碼:

if (parent != null) {
    c = parent.loadClass(name, false);
}

加載成功就得到Class對象c,失敗就拋異常然後前一級方法用catch抓住並忽略,再進行當前類加載器的findClass()操作,如此反覆。

注意
1. 出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類
2. 類加載後將進入連接(link)階段,它包含驗證、準備、解析,resolve參數決定是否執行解析階段,jvm規範並沒有嚴格指定該階段的執行時刻
3. 由於先使用findLoadedClass()查找緩存,相同的類只會被加載一次

用戶自定義類加載器

當你自己寫一個類實現了ClassLoader後,那麼它就是用戶自定義類加載器了。實例化自定義類加載器時,若不指定父類加載器(不把父ClassLoader傳入構造函數)的情況下,默認採用系統類加載器(AppClassLoader)。對應的無參默認構造函數實現如下:

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

它將調用有參構造函數,將getSystemClassLoader()取到的系統類加載器作爲parent傳入(最後一節詳述)。因此用戶自定義類加載器也可以通過雙親委派的方式獲取到那3個類加載器加載的類對象了。

當實現自定義類加載器時不應重寫loadClass(),除非你不需要雙親委派機制。要重寫的是findClass()的邏輯,也就是尋找並加載類的方式。

使用自定義類加載器獲取到的Class對象需通過newInstance()獲取實例,要比較具有相同類全限定名的兩個Class對象是否是同一個,取決於是否是同一類加載器加載了它們,也就是調用defineClass()的那個類加載器,而非之前委派的類加載器。

常用方法分析

java.lang.Class對象的方法

Class<?> forName(……)

這是手動加載類的常見方式,在Class類中有兩個重載:

  • public static Class<?> forName(String className)
  • public static Class<?> forName(String name, boolean initialize,
    ClassLoader loader)

第二個構造函數指定了父類加載器,這兒可能要有疑問了,第一個方法默認使用哪個類加載器來加載的呢?我們來看下具體實現:

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    // 使用native方法獲取調用類的Class對象
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

其中getClassLoader(caller)設置了所使用的類加載器,繼續看其實現:

 static ClassLoader getClassLoader(Class<?> caller) {
     if (caller == null) {
         return null;
     }
     return caller.getClassLoader0();
 }
}

這段代碼的官方註解是“返回caller的類加載器”,即native方法getClassLoader0()返回調用者的類加載器。也就是說假設在A類裏執行forName(String className),那麼所使用的ClassLoader就是加載A的ClassLoader。

提示
forName0()本質還是調用ClassLoaderloadClass()來加載類。

ClassLoader getClassLoader()

該方法用於獲取加載某Class對象的類加載器,可是通過實例或類對象來獲取:

  • (new A()).getClass().getClassLoader()
  • A.class.getClassLoader()

各種獲取類信息的方法

反射得到Class對象後通過以下方法獲取類信息:

Field[] getDeclaredFields()

Class[] getDeclaredClasses()

Method[] getDeclaredMethods()

等等

詳情可查閱javadoc或查看源碼

java.lang.ClassLoader對象的方法

ClassLoader getParent()

獲取父ClassLoader

Class loadClass(String)

顯式調用該方法來進行類加載,傳入類全限定名

URL getResource(String)

獲取具有給定名稱的資源定位符。資源可以是任何數據,名稱須以“/”分離路徑名。實際調用findResource()方法,該方法無實現,需子類繼承實現。

InputStream getResourceAsStream(String)

獲取可以讀取資源的InputStream輸入流,實際上就是用上面的方法獲取到URL後調用url.openStream()得到 InputStream。

ClassLoader getSystemClassLoader()

這是一個靜態方法,通過ClassLoader.getSystemClassLoader()便可獲取到系統類加載器AppClassLoader, 和調用類無關。具體實現見最後一小節。

URLClassLoader

概述

ClassLoader只是一個抽象類,很多方法是空的需要自己去實現,比如 findClass()findResource()等。而java提供了java.net.URLClassLoader這個實現類,適用於多種應用場景。

之前提到的AppClassLoaderExtClassLoader都是URLClassLoader的子類,自定義類加載器推薦直接繼承它。

來看下javadoc中的描述:

該類加載器用於從一組URL路徑(指向JAR包或目錄)中加載類和資源。約定使用以 ‘/’結束的URL來表示目錄。如果不是以該字符結束,則認爲該URL指向一個JAR文件。

構造函數

URLClassLoader接受一個URL數組爲參數,它將在這些提供的路徑下加載所需要的類,對應的主要構造函數有

  • public URLClassLoader(URL[] urls)
  • URLClassLoader(URL[] urls, ClassLoader parent)

getURLs()方法

使用URL[] getURLs()方法可以獲取URL路徑,參考代碼:

public static void main(String[] args) {
    URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
    for (URL url : urls) {
        System.out.println(url);
    }
}
// file:/D:/Workbench/Test/bin/

加載方式

findClass()中其使用了URLClassPath類中的Loader類來加載類文件和資源。URLClassPath類中定義了兩個Loader類的實現,分別是FileLoaderJarLoader類,顧名思義前者用於加載目錄中的類和資源,後者是加載jar包中的類和資源。Loader類默認已經實現getResource()方法,即從網絡URL地址加載jar包然後使用JarLoader完成後續加載,而兩個實現類不過是重寫了該方法。

URLClassPath是如何選擇使用正確的Loader的呢?答案是——根據URL格式而定。下面是刪減過的核心代碼,簡單易懂。

private Loader getLoader(final URL url)
{
    String s = url.getFile();
    // 以"/"結尾時,若url協議爲"file"則使用FileLoader加載本地文件
    // 否則使用默認的Loader加載網絡url
    if(s != null && s.endsWith("/"))
    {
        if("file".equals(url.getProtocol()))
            return new FileLoader(url);
        else
            return new Loader(url);
    } else {
        // 非"/"結尾則使用JarLoader
        return new JarLoader(url, jarHandler, lmap);
    }
}

getSystemClassLoader()方法的實現

追溯getSystemClassLoader()的源碼可以發現其實質上是通過sun.misc.Launcher實例獲取返回其成員變量loader的。那這個loader是何時賦值的呢?我們來看下它的構造函數(刪減了不相關的內容):

  public Launcher()
  {
      ExtClassLoader extclassloader;
      try
      {
      // 創建並初始化擴展類加載器ExtClassLoader
          extclassloader = ExtClassLoader.getExtClassLoader();
      }
      catch(IOException ioexception)
      {
          throw new InternalError("Could not create extension class loader");
      }
      try
      {
          // 創建並初始化系統類加載器AppClassLoader,設置其父類加載器爲ext,最後傳給loader
          loader = AppClassLoader.getAppClassLoader(extclassloader);
      }
      catch(IOException ioexception1)
      {
          throw new InternalError("Could not create application class loader");
      }
      // 默認將線程上下文類加載器設置爲AppClassLoader
      // 相關信息見另一篇博文
      Thread.currentThread().setContextClassLoader(loader);
  }

可以看到Launcher初始化時創建生成了ExtClassLoaderAppClassLoader,並將線程上下文類加載器默認設置爲了AppClassLoader。雖然沒去看jvm的源碼,但我推測jvm可能就是通過創建Launcher實例來完成擴展和系統類加載器的創建的,而啓動(Bootstrap)類加載器的創建則是另外調用本地方法完成的。

很明顯,getSystemClassLoader()返回的loader就是AppClassLoader無誤,這兒我們也發現了線程上下文類加載器賦值處,具體有關線程上下文類加載器的學習請參考底部的另一篇博文。

總結

通常需要你自己寫類加載器的場景不多,但通過上述對類加載器的分析研究至少可以讓你瞭解jvm的底層實現機制以及熟悉反射的實現方式。我個人的風格就是知其然知其所以然,在我理解範圍內的知識我都有興趣去研究。之前總是花一整段時間去啃下難點後就置之不理了,工作後才養成這種常記筆記的習慣,自己總結梳理後的確比看別人的文章要來得更深刻更透徹,望繼續保持!





延伸閱讀:真正理解線程上下文類加載器(多案例分析)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章