Tomcat 第六篇:類加載機制

1. 引言
Tomcat 在部署 Web 應(yīng)用的時候,是將應(yīng)用放在 webapps 文件夾目錄下,而 webapps 對應(yīng)到 Tomcat 中是容器 Host ,里面的文件夾則是對應(yīng)到 Context ,在 Tomcat 啟動以后, webapps 中的所有的 Web 應(yīng)用都可以提供服務(wù)。
這里會涉及到一個問題, webapps 下面不止會有一個應(yīng)用,比如有 APP1 和 APP2 兩個應(yīng)用,它們分別有自己獨立的依賴 jar 包,這些 jar 包會位于 APP 的 WEB-INFO/lib 這個目錄下,這些 jar 包大概率是會有重復(fù)的,比如常用的 Spring 全家桶,在這里面,版本肯定會有不同,那么 Tomcat 是如何處理的?
2. JVM 類加載機制
說到 Tomcat 的類加載機制,有一個繞不開的話題是 JVM 是如何進行類加載的,畢竟 Tomcat 也是運行在 JVM 上的。
以下內(nèi)容參考自周志明老師的 「深入理解 Java 虛擬機」。
2.1 什么是類的加載
類的加載指的是將類的 .class 文件中的二進制數(shù)據(jù)讀入到內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個 java.lang.Class 對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的 Class 對象, Class 對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向 Java 程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
類加載器并不需要等到某個類被 「首次主動使用」 時再加載它, JVM 規(guī)范允許類加載器在預(yù)料某個類將要被使用時就預(yù)先加載它,如果在預(yù)先加載的過程中遇到了 .class 文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤( LinkageError 錯誤)如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤。
加載.class文件的方式
–?從本地系統(tǒng)中直接加載
–?通過網(wǎng)絡(luò)下載.class文件
–?從zip,jar等歸檔文件中加載.class文件
–?從專有數(shù)據(jù)庫中提取.class文件
–?將Java源文件動態(tài)編譯為.class文件
2.2 類生命周期
接下來,我們看下一個類的生命周期:
一個類型從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking)。

2.3 雙親委派模型
Java 提供三種類型的系統(tǒng)類加載器:
啟動類加載器(Bootstrap ClassLoader):由 C++ 語言實現(xiàn),屬于 JVM 的一部分,其作用是加載 目錄中的文件,或者被\lib -Xbootclasspath參數(shù)所指定的路徑中的文件,并且該類加載器只加載特定名稱的文件(如 rt.jar ),而不是該目錄下所有的文件。啟動類加載器無法被 Java 程序直接引用。擴展類加載器( Extension ClassLoader ):由 sun.misc.Launcher.ExtClassLoader實現(xiàn),它負(fù)責(zé)加載目錄中的,或者被\lib\ext java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器。應(yīng)用程序類加載器( Application ClassLoader ):也稱系統(tǒng)類加載器,由 sun.misc.Launcher.AppClassLoader實現(xiàn)。負(fù)責(zé)加載用戶類路徑( Class Path )上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。

「雙親委派模型的工作機制:」
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
「為什么?」
例如類 java.lang.Object ,它存放在 rt.jar 之中。無論哪一個類加載器都要加載這個類。最終都是雙親委派模型最頂端的 Bootstrap 類加載器去加載。因此 Object 類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶編寫了一個稱為 「java.lang.Object」 的類,并存放在程序的 ClassPath 中,那系統(tǒng)中將會出現(xiàn)多個不同的 Object 類, java 類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會一片混亂。
3. Tomcat 類加載機制
先整體看下 Tomcat 類加載器:

可以看到,在原來的 JVM 的類加載機制上面, Tomcat 新增了幾個類加載器,包括 3 個基礎(chǔ)類加載器和每個 Web 應(yīng)用的類加載器。
3 個基礎(chǔ)類加載器在 conf/catalina.properties 中進行配置:
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
Common: 以應(yīng)用類加載器為父類,是 Tomcat 頂層的公用類加載器,其路徑由 conf/catalina.properties中的common.loader指定,默認(rèn)指向${catalina.home}/lib下的包。Catalina: 以 Common 類加載器為父類,是用于加載 Tomcat 應(yīng)用服務(wù)器的類加載器,其路徑由 server.loader指定,默認(rèn)為空,此時 Tomcat 使用 Common 類加載器加載應(yīng)用服務(wù)器。Shared: 以 Common 類加載器為父類,是所有 Web 應(yīng)用的父類加載器,其路徑由 shared.loader指定,默認(rèn)為空,此時 Tomcat 使用 Common 類加載器作為 Web 應(yīng)用的父加載器。Web 應(yīng)用: 以 Shared 類加載器為父類,加載 /WEB-INF/classes目錄下的未壓縮的 Class 和資源文件以及/WEB-INF/lib目錄下的 jar 包,該類加載器只對當(dāng)前 Web 應(yīng)用可見,對其他 Web 應(yīng)用均不可見。
4. Tomcat 類加載機制源碼
4.1 ClassLoader 的創(chuàng)建
先看下加載器類圖:

先從 BootStrap 的 main 方法看起:
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 設(shè)置到了當(dāng)前線程,如果為 null 下面是走到了 init() 方法。
public?void?init()?throws?Exception?{
????//?初始化類加載器
????initClassLoaders();
????//?設(shè)置線程類加載器,將容器的加載器傳入
????Thread.currentThread().setContextClassLoader(catalinaLoader);
????//?設(shè)置區(qū)安全類加載器
????SecurityClassLoad.securityClassLoad(catalinaLoader);
????//?省略其余代碼...
}
接著這里看到了會調(diào)用 initClassLoaders() 方法進行類加載器的初始化,初始化完成后,同樣會設(shè)置 Catalina ClassLoader 到當(dāng)前線程。
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);
????}
}
看到這里應(yīng)該就清楚了,會創(chuàng)建三個 ClassLoader :CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好對應(yīng)前面介紹的三個基礎(chǔ)類加載器。
接著進入 createClassLoader() 查看代碼:
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 中實現(xiàn)的。
4.2.1 第一步:
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;
????????}
????????//?省略其余...
首先調(diào)用 findLoaderClass0() 方法檢查 WebappClassLoader 中是否加載過此類。
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 緩存中。
protected?final?Map?resourceEntries?=?new?ConcurrentHashMap<>();
4.2.2 第二步:
????//?省略其余...
????clazz?=?findLoadedClass(name);
????if?(clazz?!=?null)?{
????????if?(log.isDebugEnabled())
????????????log.debug("??Returning?class?from?cache");
????????if?(resolve)
????????????resolveClass(clazz);
????????return?clazz;
????}
????//?省略其余...
如果第一步?jīng)]有找到,則繼續(xù)檢查 JVM 虛擬機中是否加載過該類。調(diào)用 ClassLoader 的 findLoadedClass() 方法檢查。
4.2.3 第三步:
????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
????????}
????}
如果前兩步都沒有找到,則使用系統(tǒng)類加載該類(也就是當(dāng)前 JVM 的 ClassPath )。為了防止覆蓋基礎(chǔ)類實現(xiàn),這里會判斷 class 是不是 JVMSE 中的基礎(chǔ)類庫中類。
4.2.4 第四步:
????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
????????}
????}
先判斷是否設(shè)置了 delegate 屬性,設(shè)置為 true ,那么就會完全按照 JVM 的"雙親委托"機制流程加載類。
若是默認(rèn)的話,是先使用 WebappClassLoader 自己處理加載類的。當(dāng)然,若是委托了,使用雙親委托亦沒有加載到 class 實例,那還是最后使用 WebappClassLoader 加載。
4.2.5 ?第五步:
????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
????}
若是沒有委托,則默認(rèn)會首次使用 WebappClassLoader 來加載類。通過自定義 findClass() 定義處理類加載規(guī)則。
findClass() 會去 Web-INF/classes 目錄下查找類。
4.2.6 ?第六步:
????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 小結(jié)
Web 應(yīng)用類加載器默認(rèn)的加載順序是:
先從緩存中加載; 如果沒有,則從 JVM 的 Bootstrap 類加載器加載; 如果沒有,則從當(dāng)前類加載器加載(按照 WEB-INF/classes 、 WEB-INF/lib 的順序); 如果沒有,則從父類加載器加載,由于父類加載器采用默認(rèn)的委派模式,所以加載順序是 AppClassLoader 、 Common 、 Shared 。
參考
https://www.jianshu.com/p/69c4526b843d

