深入理解Java類加載器

本文主要內(nèi)容

  • 類加載器基本概念
  • 自定義類加載器
  • 類的隔離
  • Android類加載器案例

虛擬機類加載機制 文中已經(jīng)對類加載機制詳細(xì)闡述了,這兩天對類的隔離,破壞雙親委托機制等內(nèi)容有了新的理解,同時闡述下Android上類加載器案例。

以雙親委托機制圖鎮(zhèn)樓:

類加載器基本概念

顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個實例。

有了Class類實例,就可以通過newInstance方法創(chuàng)建該類的對象。

一般來說,默認(rèn)類加載器為當(dāng)前類的類加載器。比如A類中引用B類,A的類加載器為C,那么B的類加載器也為C。

1、ClassLoader

ClassLoader類是一個抽象類,它定義了類加載器的基本方法。

方法 說明
getParent() 返回該類加載器的父類加載器。
loadClass(String name) 加載名稱為 name的類,返回的結(jié)果是 java.lang.Class類的實例。
findClass(String name) 查找名稱為 name的類,返回的結(jié)果是 java.lang.Class類的實例。
findLoadedClass(String name) 查找名稱為 name的已經(jīng)被加載過的類,返回的結(jié)果是 java.lang.Class類的實例。
defineClass(String name, byte[] b, int off, int len) 把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類,返回的結(jié)果是 java.lang.Class類的實例。這個方法被聲明為 final的。

來看看 loadClass 方法的代碼:

protected Class<?> loadClass(String name, boolean resolve){
    Class c = findLoadedClass(name);
    if (c == null) {
        if (parent != null) {
            //使用父加載器加載此類
            c = parent.loadClass(name, false);
        } 
        if (c == null) {
            // 如果父加載器沒有成功加載,則自己嘗試加載
            c = findClass(name);
        }
    }
    return c;
}

這段代碼定義了雙親委托模型。所以自定義類加載器盡量不要去重寫 loadClass ,而應(yīng)該重寫 findClass 方法。下邊讓我們來實現(xiàn)一個自定義類加載器。

自定義類加載器

自定義類加載器還是很有必要的,尤其是在web服務(wù)器上,比如tomcat,自定義加載器能夠指定自身加載類的范圍,甚至通過繼承關(guān)系,達(dá)到類的隔離目的。

Android上也有自定義類加載器,Android上的策略是,每個apk都由不同的類加載器實例來加載。思考一下,如果兩個apk中有相同名字的類,如果由同一個類加載器實例來加載,那肯定會混淆。另一方面也是安全問題。

通過前一章的學(xué)習(xí),可知自定義類加載器一般只重寫findClass方法即可。真正完成類的加載工作是通過調(diào)用 defineClass來實現(xiàn)的;而啟動類的加載過程是通過調(diào)用 loadClass來實現(xiàn)的。

  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"; 
   } 
}

類的隔離

類的隔離主要有以下兩點:

  • 不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間,不同類加載器加載的類是不兼容的。
  • 為了安全或其它目的,使某個模塊無法加載某個類。

第1點比較簡單,網(wǎng)上有很多這方面的文章,在此不再闡述。

第2點稍等復(fù)雜點,以tomcat為例,tomcat服務(wù)器所用到的jar包不希望web應(yīng)用應(yīng)用,于是tomcat設(shè)計了以下經(jīng)典的類加載模型:

其中webApp應(yīng)用的類加載器為 WebApp類加載器,而tomcat服務(wù)器類加載器為 Catalina類加載器,如果webApp想加載tomcat所使用的jar包,它先會委托它的父加載器去加載,根據(jù)上圖所示,它的父加載器也無法加載(因為tomcat所引用jar包全由Catalina加載,而Catalina并不是WebApp的父加載器),由于它自己也無法加載,所以實現(xiàn)隔離。

在雙親委托機制下,同級的類加載器,可以實現(xiàn)類的隔離。

另外,頂層類加載器限制較大,有時它無法加載類也無法委托子類加載器去加載,也會導(dǎo)致隔離。

1、線程上下文類加載器

JDBC是Java開發(fā)者經(jīng)常遇到的內(nèi)容,它的核心類為java.sql.DriverManager,查看它的包名,就知道此類是由 啟動類加載器(Bootstrap ClassLoader)加載,但JDBC的具體驅(qū)動實現(xiàn)是由各個廠商自己實現(xiàn)的。DriverManager需要調(diào)用由廠商自己實現(xiàn)的接口,這些接口是由 用戶程序類加載器(Application ClassLoader)加載。

由前文知,DriverManager由Bootstrap加載,當(dāng)DriverManager引用廠商實現(xiàn)的JDBC接口時,DriverManager仍然會使用自己的類加載器,也就是Bootstrap去加載,但Bootstrap只能加載JAVA_HOME下的class文件,廠商實現(xiàn)的JDBC接口,Bootstrap無法加載。這種問題,雙親委托模型已經(jīng)無法解決了。

為了解決此問題,Java開了后門,也就是添加了線程上下文類加載器,Java為每個線程設(shè)置了默認(rèn)的線程上下文類加載器:Application ClassLoader,當(dāng)出現(xiàn)上述情況時,直接使用線程上下文類加載器加載。

  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);

在JDBC的例子中,DriverManager不再使用自己的類加載器(Bootstrap)去加載,而是使用線程上下文類加載器去加載,而線程上下文類加載器就是 用戶程序類加載器(Application ClassLoader),這當(dāng)然能加載類成功。

這種父類加載器請求子類加載器去加載類的行為,實質(zhì)上已經(jīng)破壞了雙親委托模型。

2、Class.forName

JDBC在使用之前,一定要調(diào)用一句話,Class.forName,很多人告訴我,這是要去加載驅(qū)動類。

  Class.forName("com.mysql.jdbc.Driver");

仔細(xì)想一想,這不對,如果代碼引用了某個類,會去自動加載類,不需要用戶手動加載,除非是當(dāng)前的類加載器無法加載此類。

查看Driver類的源碼:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  //
  // Register ourselves with the DriverManager
  //
  static {
      try {
          java.sql.DriverManager.registerDriver(new Driver());
      } catch (SQLException E) {
          throw new RuntimeException("Can't register driver!");
      }
  }
}

類的加載,會有一個初始化階段,在初始化階段會執(zhí)行類的 clinit 方法,也就是會執(zhí)行類的靜態(tài)語句塊。

通過以上線索發(fā)現(xiàn),其實調(diào)用 Class.forName并不是要加載驅(qū)動類,而是調(diào)用驅(qū)動類的靜態(tài)語句塊,向DriverManager注冊自己而已。

Android類加載器案例

Android apk動態(tài)加載研究 文中提到了Context的類加載器與當(dāng)前apk的類加載器相同,其實這句話是不對的,只是Context類重寫了getClassLoader方法。

public ClassLoader getClassLoader() {
    return mPackageInfo != null ?
            mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}

并不是Context的類加載器不同,而是通過LoadedApk獲取的類加載器不同。

  mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
                mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                libraryPermittedPath, mBaseClassLoader);

LoadedApk的getClassLoader方法中,根據(jù)apk的路徑等參數(shù),生成了新的ClassLoader,以確保不同的apk對應(yīng)著不同的ClassLoader。

  PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
                                                  zip,
                                                  librarySearchPath,
                                                  libraryPermittedPath,
                                                  parent,
                                                  targetSdkVersion,
                                                  isBundled);

比如說,在ActivityThread類中,生成新的Activity時,就使用了新生成的Classloader,確保不同apk的Activity是由不同Classloader生成的。

  Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        。。。
    }

newActivity的代碼如下:

  public Activity newActivity(ClassLoader cl, String className,
        Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

其實就是調(diào)用ClassLoader 加載具體Activity全類名,然后調(diào)用newInstance生成一個新的對象。

閱讀源碼往往有意想不到的收獲,當(dāng)時懷疑為啥Context的類加載器不一樣,一讀源碼,收獲還挺多的,大家遇到疑問多多讀源碼吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容