java與tomcat7類載入機制
1. java類載入器
近來了解tomcat的類載入機制,所以先回顧一下java虛擬機器類載入器,如果從java虛擬機器的角度來看的話,其實類載入器只分為兩種:一種是啟動類載入器(即Bootstrap ClassLoader),通過使用JNI來實現,我們無法獲取到到它的例項;另一種則是java語言實現java.lang.ClassLoader
的子類。一般從我們的角度來看,會根據類載入路徑會把類載入器分為3種:Bootstrap ClassLoader,ExtClassLoader,AppClassLoader.後兩者是sun.misc.Launcher
類的內部類,而前者在JDK原始碼中是沒有與之對應的類的,倒是在sun.misc.Launcher
Bootstrap ClassLoader: 引導類載入器,從%JAVA_RUNTIME_JRE%/lib目錄載入,但並不是將該目錄所有的類庫都載入,它會載入一些符合檔名稱的,例如:rt.jar,resources.jar等。在sun.misc.Launcher
原始碼中也可以看得它的載入路徑:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
或者配置-Xbootclasspath引數指定載入的路徑,通過獲取環境變數sun.boot.class.path
D:\Program Files\Java\jdk1.7.0_67\jre\lib\resources.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\rt.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jce.jar
D:\Program Files\Java\jdk 1.7.0_67\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.7.0_67\jre\classes
Extension ClassLoader:擴充套件類載入器,實現類為sun.misc.Launcher$ExtClassLoader
,載入%JAVA_RUNTIME_JRE%/lib/ext/目錄下的jar包,也可以在sun.misc.Launcher
原始碼中也可以看得它的載入路徑:
String s = System.getProperty("java.ext.dirs");
通過獲取java.ext.dirs
環境變數列印一下:
D:\Program Files\Java\jdk1.7.0_67\jre\lib\ext
Appication ClassLoader:應用程式類載入器,或者叫系統類載入器,實現類為sun.misc.Launcher$AppClassLoader
。從sun.misc.Launcher
的建構函式中可以看到,當AppClassLoader
被初始化以後,它會被設定為當前執行緒的上下文類載入器以及儲存到Launcher
類的loader屬性中,而通過ClassLoader.getSystemClassLoader()
獲取的也正是該類載入器(Launcher.loader)。應用類載入器從使用者類路徑中載入類庫,可以在原始碼中看到:
final String s = System.getProperty("java.class.path");
1.1 類關係
由圖看到Bootstrap ClassLoader並不在繼承鏈上,因為它是java虛擬機器內建的類載入器,對外不可見。可以看到頂層ClassLoader
有一個parent屬性,用來表示著類載入器之間的層次關係(雙親委派模型);注意,ExtClassLoader
類在初始化時顯式指定了parent為null,所以它的父類載入器預設為Bootstrap ClassLoader
。在tomcat中都是通過擴充套件URLClassLoader
來實現自己的類載入器。
1.2 雙親委託模型
這3種類載入器之間存在著父子關係(區別於java裡的繼承),子載入器儲存著父載入器的引用。當一個類載入器需要載入一個目標類時,會先委託父載入器去載入,然後父載入器會在自己的載入路徑中搜索目標類,父載入器在自己的載入範圍中找不到時,才會交還給子載入器載入目標類。
採用雙親委託模式可以避免類載入混亂,而且還將類分層次了,例如java中lang包下的類在jvm啟動時就被啟動類載入器載入了,而使用者一些程式碼類則由應用程式類載入器(AppClassLoader)載入,基於雙親委託模式,就算使用者定義了與lang包中一樣的類,最終還是由應用程式類載入器委託給啟動類載入器去載入,這個時候啟動類載入器發現已經載入過了lang包下的類了,所以兩者都不會再重新載入。當然,如果使用者通過自定義的類載入器可以強行打破這種雙親委託模型,但也不會成功的,java安全管理器丟擲將會丟擲java.lang.SecurityException
異常。
- 啟動類載入器是擴充套件類載入器的父類載入器:擴充套件類載入器在
sun.misc.Launcher
建構函式中被初始化,它的父類載入器被設定了為null,那為什麼還說啟動類載入器是它的父載入器?看一下ClassLoader.loadClass()
方法:
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,則呼叫本地方法,交由啟動類載入器載入,所以說ExtClassLoader的父類載入器為Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) { //仍然載入不到,只能由本載入器通過findClass去載入
long t1 = System.nanoTime();
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;
}
}
從程式碼中看到,如果parent==null,將會由啟動類載入器嘗試載入,所以擴充套件類載入器的父類載入器是啟動類載入器。
- 擴充套件類載入器是應用程式類載入器的父類載入器:這個比較好理解,依然是在
sun.misc.Launcher
建構函式初始化應用程式類載入器時,指定了ExtClassLoader為AppClassLoader的父類載入器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的屬性,extcl是擴充套件類載入器例項
- 應用程式類載入器是自定義類載入器的父類載入器:這裡指的是使用預設建構函式進行自定義類載入器(否則 你可以指定parent來構造一個父載入器為ExtClassLoader的自定義類載入器),無論是通過擴充套件ClassLoader還是URLClassLoader最終都會獲取系統類載入器(AppClassLoader)作為父類載入器:
protected ClassLoader() {
//呼叫getSystemClassLoader方法獲取系統類載入器作為父類載入器
this(checkCreateClassLoader(), getSystemClassLoader());
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader(); //初始化系統類載入器
.....
return scl;
}
private static synchronized void initSystemClassLoader() {
......
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
......
scl = l.getClassLoader(); //這裡拿到的就是在Launcher建構函式中構造的AppClassLoader例項
......
}
}
2. tomcat7類載入器
tomcat作為一個java web容器,也有自己的類載入機制,通過自定義的類載入機制以實現共享類庫的抽取,不同web應用之間的資源隔離還有熱載入等功能。除了一些java自身的一些類載入器處,它實現的主要類載入器有:Common ClassLoader,Catalina ClassLoader,Shared ClassLoader以及WebApp ClassLoader.通過下面類關係圖以及邏輯關係圖,同時對比上文內容梳理這些類載入器之間的關係。
2.1 類關係圖
從圖中看到了Common,Catalina,Shared類載入器是URLClassLoader類的一個例項,只是它們的類載入路徑不一樣,在tomcat/conf/catalina.properties配置檔案中配置(common.loader,server.loader,shared.loader).WebAppClassLoader繼承自WebAppClassLoaderBase,基本所有邏輯都在WebAppClassLoaderBase為中實現了,可以看出tomcat的所有類載入器都是以URLClassLoader為基礎進行擴充套件。
2.2 邏輯關係圖
上面說到Common,Catalina,Shared類載入器是URLClassLoader類的一個例項,在預設的配置中,它們其實都是同一個物件,即commonLoader,結合初始化時的程式碼(只保留關鍵程式碼):
private void initClassLoaders() {
commonLoader = createClassLoader("common", null); // commonLoader的載入路徑為common.loader
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader); // 載入路徑為server.loader,預設為空,父類載入器為commonLoader
sharedLoader = createClassLoader("shared", commonLoader); // 載入路徑為shared.loader,預設為空,父類載入器為commonLoader
}
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent; // catalinaLoader與sharedLoader的載入路徑均為空,所以直接返回commonLoader物件,預設3者為同一個物件
}
在上面的程式碼初始化時很明確是指出了,catalina與shared類載入器的父類載入器為common類載入器,而初始化commonClassLoader時父類載入器設定為null,最終會調到createClassLoader
靜態方法:
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
.....
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array); //該構造方法預設獲取系統類載入器為父類載入器,即AppClassLoader
else
return new URLClassLoader(array, parent);
}
});
}
在createClassLoader
中指定引數parent==null
時,最終會以系統類載入器(AppClassLoader)作為父類載入器,這解釋了為什麼commonClassLoader的父類載入器是AppClassLoader.
一個web應用對應著一個StandardContext
例項,每個web應用都擁有獨立web應用類載入器(WebClassLoader),這個類載入器在StandardContext.startInternal()
中被構造了出來:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
這裡getParentClassLoader()
會獲取父容器StandarHost.parentClassLoader
物件屬性,而這個物件屬性是在Catalina$SetParentClassLoaderRule.begin()
初始化,初始化的值其實就是Catalina.parentClassLoader
物件屬性,再來跟蹤一下Catalina.parentClassLoader
,在Bootstrap.init()
時通過反射呼叫了Catalina.setParentClassLoader()
,將Bootstrap.sharedLoader
屬性設定為Catalina.parentClassLoader
,所以WebClassLoader的父類載入器是Shared ClassLoader.
2.3 類載入邏輯
tomcat的類載入機制是違反了雙親委託原則的,對於一些未載入的非基礎類(Object,String等),各個web應用自己的類載入器(WebAppClassLoader)會優先載入,載入不到時再交給commonClassLoader走雙親委託。具體的載入邏輯位於WebAppClassLoaderBase.loadClass()
方法中,程式碼篇幅長,這裡以文字描述載入一個類過程:
- 先在本地快取中查詢是否已經載入過該類(對於一些已經載入了的類,會被快取在
resourceEntries
這個資料結構中),如果已經載入即返回,否則 繼續下一步。 - 讓系統類載入器(AppClassLoader)嘗試載入該類,主要是為了防止一些基礎類會被web中的類覆蓋,如果載入到即返回,返回繼續。
- 前兩步均沒載入到目標類,那麼web應用的類載入器將自行載入,如果載入到則返回,否則繼續下一步。
- 最後還是載入不到的話,則委託父類載入器(Common ClassLoader)去載入。
第3第4兩個步驟的順序已經違反了雙親委託機制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();
等很多地方都一樣是違反了雙親委託。