Tomcat 第六篇:類載入機制
阿新 • • 發佈:2020-10-09
![](https://cdn.geekdigging.com/java/tomcat/tomcat_header.jpg)
## 1. 引言
Tomcat 在部署 Web 應用的時候,是將應用放在 webapps 資料夾目錄下,而 webapps 對應到 Tomcat 中是容器 Host ,裡面的資料夾則是對應到 Context ,在 Tomcat 啟動以後, webapps 中的所有的 Web 應用都可以提供服務。
這裡會涉及到一個問題, webapps 下面不止會有一個應用,比如有 APP1 和 APP2 兩個應用,它們分別有自己獨立的依賴 jar 包,這些 jar 包會位於 APP 的 WEB-INFO/lib 這個目錄下,這些 jar 包大概率是會有重複的,比如常用的 Spring 全家桶,在這裡面,版本肯定會有不同,那麼 Tomcat 是如何處理的?
## 2. JVM 類載入機制
說到 Tomcat 的類載入機制,有一個繞不開的話題是 JVM 是如何進行類載入的,畢竟 Tomcat 也是執行在 JVM 上的。
以下內容參考自周志明老師的 「深入理解 Java 虛擬機器」。
### 2.1 什麼是類的載入
類的載入指的是將類的 .class 檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個 java.lang.Class 物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的 Class 物件, Class 物件封裝了類在方法區內的資料結構,並且向 Java 程式設計師提供了訪問方法區內的資料結構的介面。
類載入器並不需要等到某個類被 「首次主動使用」 時再載入它, JVM 規範允許類載入器在預料某個類將要被使用時就預先載入它,如果在預先載入的過程中遇到了 .class 檔案缺失或存在錯誤,類載入器必須在程式首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤。
```shell
載入.class檔案的方式
– 從本地系統中直接載入
– 通過網路下載.class檔案
– 從zip,jar等歸檔檔案中載入.class檔案
– 從專有資料庫中提取.class檔案
– 將Java原始檔動態編譯為.class檔案
```
### 2.2 類生命週期
接下來,我們看下一個類的生命週期:
一個型別從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期將會經歷載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連線(Linking)。
![](https://cdn.geekdigging.com/java/tomcat/tomcat6/java_class_life_cycle.png)
### 2.3 雙親委派模型
Java 提供三種類型的系統類載入器:
- 啟動類載入器(Bootstrap ClassLoader):由 C++ 語言實現,屬於 JVM 的一部分,其作用是載入 `\lib` 目錄中的檔案,或者被 `-Xbootclasspath` 引數所指定的路徑中的檔案,並且該類載入器只加載特定名稱的檔案(如 rt.jar ),而不是該目錄下所有的檔案。啟動類載入器無法被 Java 程式直接引用。
- 擴充套件類載入器( Extension ClassLoader ):由 `sun.misc.Launcher.ExtClassLoader` 實現,它負責載入 `\lib\ext` 目錄中的,或者被 `java.ext.dirs` 系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
- 應用程式類載入器( Application ClassLoader ):也稱系統類載入器,由 `sun.misc.Launcher.AppClassLoader` 實現。負責載入使用者類路徑( Class Path )上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
![](https://cdn.geekdigging.com/java/tomcat/tomcat6/jvm_class_loader.png)
**雙親委派模型的工作機制:**
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
**為什麼?**
例如類 java.lang.Object ,它存放在 rt.jar 之中。無論哪一個類載入器都要載入這個類。最終都是雙親委派模型最頂端的 Bootstrap 類載入器去載入。因此 Object 類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者編寫了一個稱為 「java.lang.Object」 的類,並存放在程式的 ClassPath 中,那系統中將會出現多個不同的 Object 類, java 型別體系中最基礎的行為也就無法保證,應用程式也將會一片混亂。
## 3. Tomcat 類載入機制
先整體看下 Tomcat 類載入器:
![](https://cdn.geekdigging.com/java/tomcat/tomcat6/tomcat_class_loader.png)
可以看到,在原來的 JVM 的類載入機制上面, Tomcat 新增了幾個類載入器,包括 3 個基礎類載入器和每個 Web 應用的類載入器。
3 個基礎類載入器在 `conf/catalina.properties` 中進行配置:
```shell
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
```
- Common: 以應用類載入器為父類,是 Tomcat 頂層的公用類載入器,其路徑由 `conf/catalina.properties` 中的 `common.loader` 指定,預設指向 `${catalina.home}/lib` 下的包。
- Catalina: 以 Common 類載入器為父類,是用於載入 Tomcat 應用伺服器的類載入器,其路徑由 `server.loader` 指定,預設為空,此時 Tomcat 使用 Common 類載入器載入應用伺服器。
- Shared: 以 Common 類載入器為父類,是所有 Web 應用的父類載入器,其路徑由 `shared.loader` 指定,預設為空,此時 Tomcat 使用 Common 類載入器作為 Web 應用的父載入器。
- Web 應用: 以 Shared 類載入器為父類,載入 `/WEB-INF/classes` 目錄下的未壓縮的 Class 和資原始檔以及 `/WEB-INF/lib` 目錄下的 jar 包,該類載入器只對當前 Web 應用可見,對其他 Web 應用均不可見。
## 4. Tomcat 類載入機制原始碼
### 4.1 ClassLoader 的建立
先看下載入器類圖:
![](https://cdn.geekdigging.com/java/tomcat/tomcat6/WebappClassLoaderBase.png)
先從 BootStrap 的 main 方法看起:
```java
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
// 省略其餘程式碼...
}
}
```
可以看到這裡先判斷了 bootstrap 是否為 null ,如果不為 null 直接把 Catalina ClassLoader 設定到了當前執行緒,如果為 null 下面是走到了 init() 方法。
```java
public void init() throws Exception {
// 初始化類載入器
initClassLoaders();
// 設定執行緒類載入器,將容器的載入器傳入
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 設定區安全類載入器
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 省略其餘程式碼...
}
```
接著這裡看到了會呼叫 `initClassLoaders()` 方法進行類載入器的初始化,初始化完成後,同樣會設定 Catalina ClassLoader 到當前執行緒。
```java
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
```
看到這裡應該就清楚了,會建立三個 ClassLoader : CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好對應前面介紹的三個基礎類載入器。
接著進入 `createClassLoader()` 檢視程式碼:
```java
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
```
可以看到,這裡載入的資源正好是我們剛才看到的配置檔案 `conf/catalina.properties` 中的 `common.loader` , `server.loader` 和 `shared.loader` 。
### 4.2 ClassLoader 載入過程
直接開啟 ParallelWebappClassLoader ,至於為啥不是看 WebappClassLoader ,從名字上就知道 ParallelWebappClassLoader 是一個並行的 WebappClassLoader 。
然後看下 ParallelWebappClassLoader 的 loadclass 方法是在它的父類 WebappClassLoaderBase 中實現的。
#### 4.2.1 第一步:
```java
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 省略其餘...
```
首先呼叫 `findLoaderClass0()` 方法檢查 WebappClassLoader 中是否載入過此類。
```java
protected Class> findLoadedClass0(String name) {
String path = binaryNameToPath(name, true);
ResourceEntry entry = resourceEntries.get(path);
if (entry != null) {
return entry.loadedClass;
}
return null;
}
```
WebappClassLoader 載入過的類都存放在 resourceEntries 快取中。
```java
protected final Map resourceEntries = new ConcurrentHashMap<>();
```
#### 4.2.2 第二步:
```java
// 省略其餘...
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 省略其餘...
```
如果第一步沒有找到,則繼續檢查 JVM 虛擬機器中是否載入過該類。呼叫 ClassLoader 的 `findLoadedClass()` 方法檢查。
#### 4.2.3 第三步:
```java
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
```
如果前兩步都沒有找到,則使用系統類載入該類(也就是當前 JVM 的 ClassPath )。為了防止覆蓋基礎類實現,這裡會判斷 class 是不是 JVMSE 中的基礎類庫中類。
#### 4.2.4 第四步:
```java
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
```
先判斷是否設定了 delegate 屬性,設定為 true ,那麼就會完全按照 JVM 的"雙親委託"機制流程載入類。
若是預設的話,是先使用 WebappClassLoader 自己處理載入類的。當然,若是委託了,使用雙親委託亦沒有載入到 class 例項,那還是最後使用 WebappClassLoader 載入。
#### 4.2.5 第五步:
```java
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
```
若是沒有委託,則預設會首次使用 WebappClassLoader 來載入類。通過自定義 `findClass()` 定義處理類載入規則。
`findClass()` 會去 `Web-INF/classes` 目錄下查詢類。
#### 4.2.6 第六步:
```java
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
```
若是 WebappClassLoader 在 `/WEB-INF/classes` 、 `/WEB-INF/lib` 下還是查詢不到 class ,那麼無條件強制委託給 System 、 Common 類載入器去查詢該類。
#### 4.2.7 小結
Web 應用類載入器預設的載入順序是:
1. 先從快取中載入;
2. 如果沒有,則從 JVM 的 Bootstrap 類載入器載入;
3. 如果沒有,則從當前類載入器載入(按照 WEB-INF/classes 、 WEB-INF/lib 的順序);
4. 如果沒有,則從父類載入器載入,由於父類載入器採用預設的委派模式,所以載入順序是 AppClassLoader 、 Common 、 Shared 。
## 參考
https://www.jianshu.com/p/69c4