那些有趣的程式碼(二)–偏不聽父母話的 Tomcat 類載入器

  • 2019 年 10 月 30 日
  • 筆記

看 Tomcat 的源碼越看越有趣。Tomcat 的程式碼總有一種處處都有那麼一點調皮的感覺。今天就聊一聊 Tomcat 的類載入機制。

了解過 JVM 的類載入一定知道,JVM 類載入的雙親委派機制。但是 Tomcat 卻打破了 JVM 固有的雙親委派載入機制。

JVM 的類載入

首先需要明確一下類載入是什麼?

  • Java虛擬機把描述類的數據從Class文件載入到記憶體,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的載入機制。

JVM 預定義的三個載入器:

  • 啟動類載入器(Bootstrap ClassLoader):是用本地程式碼實現的類裝入器,它負責將 /lib下面的類庫載入到記憶體中(比如rt.jar)。由於引導類載入器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類載入器的引用,所以不允許直接通過引用進行操作。
  • 標準擴展類載入器(Extension ClassLoader):是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變數 java.ext.dir指定位置中的類庫載入到記憶體中。開發者可以直接使用標準擴展類載入器。
  • 應用程式類載入器(Application ClassLoader):是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫載入到記憶體中。開發者可以直接使用系統類載入器。

雙親委派機制:

所謂雙親委派機制,這裡要指出的是,其實雙親委派來源於英文的 」parents delegate「,僅僅表示的只是」父輩「,可見翻譯的人不但英文是半吊子,而且也不了解 JVM 的類載入策略,造成了很大的誤解。尤其是這個」雙「字在初學的時候給我造成了極大的干擾。所以換個說法,應該是」父輩代理「。

類載入的時候,把載入的這個動作遞歸的委託給父輩,由父輩代勞,只有父輩無法載入時,才會由自己載入。

雙親委派載入模型:

parents

這裡需要特別注意的是載入器的關係並非是繼承的關係。我們看程式碼:

static class ExtClassLoader extends URLClassLoader{      ... ...  }  static class AppClassLoader extends URLClassLoader{      ... ...  }  

二者同時繼承了 URLClassLoader ,繼承關係如下:

appClassLoader

怎麼實現委託機制呢?在 ClassLoader 裡面有幾處比較重要的程式碼:

public abstract class ClassLoader {        // The parent class loader for delegation        // Note: VM hardcoded the offset of this field, thus all new fields        // must be added *after* it.      private final ClassLoader parent;        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) {                      // 嘗試使用 父輩的 loadClass 方法                          c = parent.loadClass(name, false);                      } else {                      // 如果沒有 父輩的 classLoader 就使用 bootstrap classLoader                          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();                      // 父輩沒法載入這個 class,就自己嘗試載入                      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;          }      }        // 根據類名 尋找 class。我們在之前我們講過,不通過的 classLoader 載入的 class 的位置不同。      protected Class<?> findClass(String name) throws ClassNotFoundException {          return defineClass(name, res);      }    }  
  1. 首先在初始化 ClassLoader 的時候需要指定自己的 parent 是誰?(這很重要)
  2. 先檢查類有沒被載入,如果類已經被載入了,直接返回。
  3. 如果沒有被載入,則通過 parent 的 loadClass 來嘗試載入類。(雙親委派的核心邏輯)
  4. 找不到 parent 的時候使用 bootstrap ClassLoader 進行載入。
  5. 如果委託的 parent 沒法載入類,那就自己載入。

Tomcat 的類載入

Tomcat 自己實現了自己的類載入器 WebAppClassLoader。類圖關係圖如下:

WebAppClassLoader

我們就來看看 Tomcat 的類載入器是怎麼打破雙親委派的機制的。我們先看程式碼:

findClass

    @Override      public Class<?> findClass(String name) throws ClassNotFoundException {          // Ask our superclass to locate this class, if possible          // (throws ClassNotFoundException if it is not found)          Class<?> clazz = null;            // 先在自己的 Web 應用目錄下查找 class          clazz = findClassInternal(name);            // 找不到 在交由父類來處理          if ((clazz == null) && hasExternalRepositories) {              clazz = super.findClass(name);          }          if (clazz == null) {               throw new ClassNotFoundException(name);          }          return clazz;      }  

對於 Tomcat 的類載入的 findClass 方法:

  • 首先在 web 目錄下查找。(重要)
  • 找不到再交由父類的 findClass 來處理。
  • 都找不到,那就拋出 ClassNotFoundException。

loadClass 方法

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {          synchronized (getClassLoadingLock(name)) {              Class<?> clazz = null;              //1. 先在本地cache查找該類是否已經載入過              clazz = findLoadedClass0(name);              if (clazz != null) {                  if (resolve)                      resolveClass(clazz);                  return clazz;              }              //2. 從系統類載入器的cache中查找是否載入過              clazz = findLoadedClass(name);              if (clazz != null) {                  if (resolve)                      resolveClass(clazz);                  return clazz;              }             // 3. 嘗試用ExtClassLoader類載入器類載入              ClassLoader javaseLoader = getJavaseClassLoader();              try {                  clazz = javaseLoader.loadClass(name);                  if (clazz != null) {                      if (resolve)                          resolveClass(clazz);                      return clazz;                  }              } catch (ClassNotFoundException e) {                  // Ignore              }              // 4. 嘗試在本地目錄搜索class並載入              try {                  clazz = findClass(name);                  if (clazz != null) {                      if (resolve)                          resolveClass(clazz);                      return clazz;                  }              } catch (ClassNotFoundException e) {                  // Ignore              }              // 5. 嘗試用系統類載入器(也就是AppClassLoader)來載入                  try {                      clazz = Class.forName(name, false, parent);                      if (clazz != null) {                          if (resolve)                              resolveClass(clazz);                          return clazz;                      }                  } catch (ClassNotFoundException e) {                      // Ignore                  }             }          //6. 上述過程都載入失敗,拋出異常          throw new ClassNotFoundException(name);      }  

總結一下載入的步驟:

  1. 先在本地cache查找該類是否已經載入過,看看 Tomcat 有沒有載入過這個類。
  2. 如果Tomcat 沒有載入過這個類,則從系統類載入器的cache中查找是否載入過。
  3. 如果沒有載入過這個類,嘗試用ExtClassLoader類載入器類載入,重點來了,這裡並沒有首先使用 AppClassLoader 來載入類。這個Tomcat 的 WebAPPClassLoader 違背了雙親委派機制,直接使用了 ExtClassLoader來載入類。這裡注意 ExtClassLoader 雙親委派依然有效,ExtClassLoader 就會使用 Bootstrap ClassLoader 來對類進行載入,保證了 Jre 裡面的核心類不會被重複載入。比如在 Web 中載入一個 Object 類。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,這個載入鏈,就保證了 Object 不會被重複載入。
  4. 如果 BoostrapClassLoader,沒有載入成功,就會調用自己的 findClass 方法由自己來對類進行載入,findClass 載入類的地址是自己本 web 應用下的 class。
  5. 載入依然失敗,才使用 AppClassLoader 繼續載入。
  6. 都沒有載入成功的話,拋出異常。

總結一下以上步驟,WebAppClassLoader 載入類的時候,故意打破了JVM 雙親委派機制,繞開了 AppClassLoader,直接先使用 ExtClassLoader 來載入類。

  • 保證了基礎類不會被同時載入。
  • 由保證了在同一個 Tomcat 下不同 web 之間的 class 是相互隔離的。

more

準備把有趣的程式碼這個系列慢慢寫下去,發現編程的樂趣:

那些有趣的程式碼(一)–有點萌的 Tomcat 的執行緒池