Java虛擬機類載入器及雙親委派機制

  • 2019 年 10 月 29 日
  • 筆記

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機中的,前面《面試官,不要再問我「Java虛擬機類載入機制」了》中已經介紹了具體載入class文件的機制。本篇文章我們重點介紹載入器和雙親委派機制。

類載入器

在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bootstrap ClassLoader)、擴展類載入器(ExtClassLoader)、應用類載入器(AppClassLoader)。不同的類載入器負責不同區域的類的載入。

image

啟動類載入器:這個載入器不是一個Java類,而是由底層的c++實現,負責將存放在JAVA_HOME下lib目錄中的類庫,比如rt.jar。因此,啟動類載入器不屬於Java類庫,無法被Java程式直接引用,用戶在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,那直接使用null代替即可。

擴展類載入器:由sun.misc.Launcher$ExtClassLoader實現,負責載入JAVA_HOME下libext目錄下的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴展類載入器。

應用類載入器:由sun.misc.Launcher$AppClassLoader實現的。由於這個類載入器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系統類載入器。它負責載入用戶類路徑上所指定的類庫,可以被直接使用。如果未自定義類載入器,默認為該類載入器。

可以通過這種方式列印載入路徑及相關jar:

System.out.println("boot:" + System.getProperty("sun.boot.class.path"));  System.out.println("ext:" + System.getProperty("java.ext.dirs"));  System.out.println("app:" + System.getProperty("java.class.path"));

在列印的日誌中,可以看到詳細的路徑以及路徑下面都包含了哪些類庫。由於列印內容較多,這裡就不展示了。

類載入器的初始化

除啟動類載入器外,擴展類載入器和應用類載入器都是通過類sun.misc.Launcher進行初始化,而Launcher類則由根類載入器進行載入。相關程式碼如下:

public Launcher() {      Launcher.ExtClassLoader var1;      try {          //初始化擴展類載入器,構造函數沒有入參,無法獲取啟動類載入器          var1 = Launcher.ExtClassLoader.getExtClassLoader();      } catch (IOException var10) {          throw new InternalError("Could not create extension class loader", var10);      }        try {          //初始化應用類載入器,入參為擴展類載入器          this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);      } catch (IOException var9) {          throw new InternalError("Could not create application class loader", var9);      }        // 設置上下文類載入器      Thread.currentThread().setContextClassLoader(this.loader);       //...  }

雙親委派模型

雙親委派模型:當一個類載入器接收到類載入請求時,會先請求其父類載入器載入,依次遞歸,當父類載入器無法找到該類時(根據類的全限定名稱),子類載入器才會嘗試去載入。

image

雙親委派中的父子關係一般不會以繼承的方式來實現,而都是使用組合的關係來複用父載入器的程式碼。

通過編寫測試程式碼,進行debug,可以發現雙親委派過程中不同類載入器之間的組合關係。

image

而這一過程借用一張時序圖來查看會更加清晰。
image

ClassLoader#loadClass源碼

ClassLoader類是一個抽象類,但卻沒有包含任何抽象方法。繼承ClassLoader類並重寫findClass方法便可實現自定義類載入器。但如果破壞上面所述的雙親委派模型來實現自定義類載入器,則需要繼承ClassLoader類並重寫loadClass方法和findClass方法。

ClassLoader類的部分源碼如下:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{      //進行類載入操作時首先要加鎖,避免並發載入      synchronized (getClassLoadingLock(name)) {          //首先判斷指定類是否已經被載入過          Class<?> c = findLoadedClass(name);          if (c == null) {              long t0 = System.nanoTime();              try {                  if (parent != null) {                      //如果當前類沒有被載入且父類載入器不為null,則請求父類載入器進行載入操作                      c = parent.loadClass(name, false);                  } else {                     //如果當前類沒有被載入且父類載入器為null,則請求根類載入器進行載入操作                      c = findBootstrapClassOrNull(name);                  }              } catch (ClassNotFoundException e) {              }                if (c == null) {                  long t1 = System.nanoTime();                 //如果父類載入器載入失敗,則由當前類載入器進行載入,                  c = findClass(name);                  //進行一些統計操作                 // ...              }          }          //初始化該類          if (resolve) {              resolveClass(c);          }          return c;      }  }

上面程式碼中也提現了不同類載入器之間的層級及組合關係。

為什麼使用雙親委派模型

雙親委派模型是為了保證Java核心庫的類型安全。所有Java應用都至少需要引用java.lang.Object類,在運行時這個類需要被載入到Java虛擬機中。如果該載入過程由自定義類載入器來完成,可能就會存在多個版本的java.lang.Object類,而且這些類之間是不兼容的。

通過雙親委派模型,對於Java核心庫的類的載入工作由啟動類載入器來統一完成,保證了Java應用所使用的都是同一個版本的Java核心庫的類,是互相兼容的。

上下文類載入器

子類載入器都保留了父類載入器的引用。但如果父類載入器載入的類需要訪問子類載入器載入的類該如何處理?最經典的場景就是JDBC的載入。

JDBC是Java制定的一套訪問資料庫的標準介面,它包含在Java基礎類庫中,由根類載入器載入。而各個資料庫廠商的實現類庫是作為第三方依賴引入使用的,這部分實現類庫是由應用類載入器進行載入的。

獲取Mysql連接的程式碼:

//載入驅動程式  Class.forName("com.mysql.jdbc.Driver");  //連接資料庫  Connection conn = DriverManager.getConnection(url, user, password);

DriverManager由啟動類載入器載入,它使用到的資料庫驅動(com.mysql.jdbc.Driver)是由應用類載入器載入的,這就是典型的由父類載入器載入的類需要訪問由子類載入器載入的類。

這一過程的實現,看DriverManager類的源碼:

//建立資料庫連接底層方法  private static Connection getConnection(          String url, java.util.Properties info, Class<?> caller) throws SQLException {      //獲取調用者的類載入器      ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;      synchronized(DriverManager.class) {          //由啟動類載入器載入的類,該值為null,使用上下文類載入器          if (callerCL == null) {              callerCL = Thread.currentThread().getContextClassLoader();          }      }        //...        for(DriverInfo aDriver : registeredDrivers) {          //使用上下文類載入器去載入驅動          if(isDriverAllowed(aDriver.driver, callerCL)) {              try {                  //載入成功,則進行連接                  Connection con = aDriver.driver.connect(url, info);                  //...              } catch (SQLException ex) {                  if (reason == null) {                      reason = ex;                  }              }          }          //...      }  }

在上面的程式碼中留意改行程式碼:

callerCL = Thread.currentThread().getContextClassLoader();

這行程式碼從當前執行緒中獲取ContextClassLoader,而ContextClassLoader在哪裡設置呢?就是在上面的Launcher源碼中設置的:

// 設置上下文類載入器  Thread.currentThread().setContextClassLoader(this.loader);

這樣一來,所謂的上下文類載入器本質上就是應用類載入器。因此,上下文類載入器只是為了解決類的逆向訪問提出來的一個概念,並不是一個全新的類載入器,本質上是應用類載入器。

自定義類載入器

自定義類載入器只需要繼承java.lang.ClassLoader類,然後重寫findClass(String name)方法即可,在方法中指明如何獲取類的位元組碼流。

如果要破壞雙親委派規範的話,還需重寫loadClass方法(雙親委派的具體邏輯實現)。但不建議這麼做。

public class ClassLoaderTest extends ClassLoader {        private String classPath;        public ClassLoaderTest(String classPath) {          this.classPath = classPath;      }        /**       * 編寫findClass方法的邏輯       *       * @param name       * @return       * @throws ClassNotFoundException       */      @Override      protected Class<?> findClass(String name) throws ClassNotFoundException {          // 獲取類的class文件位元組數組          byte[] classData = getClassData(name);          if (classData == null) {              throw new ClassNotFoundException();          } else {              // 生成class對象              return defineClass(name, classData, 0, classData.length);          }      }        /**       * 編寫獲取class文件並轉換為位元組碼流的邏輯       *       * @param className       * @return       */      private byte[] getClassData(String className) {          // 讀取類文件的位元組          String path = classNameToPath(className);          try {              InputStream is = new FileInputStream(path);              ByteArrayOutputStream stream = new ByteArrayOutputStream();              byte[] buffer = new byte[2048];              int num = 0;              // 讀取類文件的位元組碼              while ((num = is.read(buffer)) != -1) {                  stream.write(buffer, 0, num);              }              return stream.toByteArray();          } catch (IOException e) {              e.printStackTrace();          }          return null;      }        /**       * 類文件的完全路徑       *       * @param className       * @return       */      private String classNameToPath(String className) {          return classPath + File.separatorChar                  + className.replace('.', File.separatorChar) + ".class";      }        public static void main(String[] args) {          String classPath = "/Users/zzs/my/article/projects/java-stream/src/main/java/";          ClassLoaderTest loader = new ClassLoaderTest(classPath);            try {              //載入指定的class文件              Class<?> object1 = loader.loadClass("com.secbro2.classload.SubClass");              System.out.println(object1.newInstance().toString());          } catch (Exception e) {              e.printStackTrace();          }      }  }

列印結果:

SuperClass static init  SubClass static init  com.secbro2.classload.SubClass@5451c3a8

關於SuperClass和SubClass在上篇文章《面試官,不要再問我「Java虛擬機類載入機制」了》已經貼過程式碼,這裡就不再貼出了。

通過上面的程式碼可以看出,主要重寫了findClass獲取class的路徑便實現了自定義的類載入器。

那麼,什麼場景會用到自定義類載入器呢?當JDK提供的類載入器實現無法滿足我們的需求時,才需要自己實現類載入器。比如,OSGi、程式碼熱部署等領域。

Java9類載入器修改

以上類載入器模型為Java8以前版本,在Java9中類載入器已經發生了變化。在這裡主要簡單介紹一下相關模型的變化,具體變化細節就不再這裡展開了。

java9中目錄的改變。
image

Java9中類載入器的改變。
image

在java9中,應用程式類載入器可以委託給平台類載入器以及啟動類載入器;平台類載入器可以委託給啟動類載入器和應用程式類載入器。

在java9中,啟動類載入器是由類庫和程式碼在虛擬機中實現的。為了向後兼容,在程式中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。但是,並不是所有的JavaSE平台和JDK模組都由啟動類載入器載入。

舉幾個例子,啟動類載入器載入的模組是java.base,java.logging,java.prefs和java.desktop。其他JavaSE平台和JDK模組由平台類載入器和應用程式類載入器載入。

java9中不再支援用於指定引導類路徑,-Xbootclasspath和-Xbootclasspath/p選項以及系統屬性sun.boot.class.path。-Xbootclasspath/a選項仍然受支援,其值存儲在jdk.boot.class.path.append的系統屬性中。

java9不再支援擴展機制。但是,它將擴展類載入器保留在名為平台類載入器的新名稱下。ClassLoader類包含一個名為getPlatformClassLoader()的靜態方法,該方法返回對平台類載入器的引用。

小結

本篇文章主要基於java8介紹了Java虛擬機類載入器及雙親委派機制,和Java8中的一些變化。其中,java9中更深層次的變化,大家可以進一步研究一下。該系列持續更新中,歡迎關注微信公眾號「程式新視界」。

原文鏈接:《Java虛擬機類載入器及雙親委派機制

《面試官》系列文章:

程式新視界:精彩和成長都不容錯過

程式新視界-微信公眾號