淺議tomcat與classloader
關於tomcat和classloader的文章,網上多如牛毛,且互相轉載,所以大多數搜到的基本上是講到了tomcat中classloader的幾個層次,對於初接觸classloader,看了之後還是隻知其然不知其所以然。
我以為,學習tomcat classloader,先得追溯到為什麼tomcat要自己造一堆自己的classloader出來,瞭解了這個之後才知道classloader用在什麼地方,什麼時候該新寫,什麼時候使用jvm的預設classloader;之後,得明白每個原因背後的道理,所謂明白,就是能將整個過程理清楚。
開始之前,不得不費點篇幅申明,關於classloader,jvm的classloader等內容,請自行查閱資料(網上搜索或jvm相關資料),做到心中有個概念,相信讀完此文,應該會加深這個印象。
一直比較好奇,為什麼tomcat需要實現自己的classloader,jvm提供的classloader有什麼不符合需要?
事實上,tomcat之所以造了一堆自己的classloader,大致是出於下面三類目的:
1. 對於各個webapp中的class和lib,需要相互隔離,不能出現一個應用中載入的類庫會影響另一個應用的情況;而對於許多應用,需要有共享的lib以便不浪費資源,舉個例子,如果webapp1和webapp2都用到了log4j,可以將log4j提到tomcat/lib中,表示所有應用共享此類庫,試想如果log4j很大,並且20個應用都分別載入,那實在是沒有必要的。
2. 第二個原因則是與jvm一樣的安全性問題。使用單獨的classloader去裝載tomcat自身的類庫,以免其他惡意或無意的破壞;
3. 第三個原因是熱部署的問題。相信大家一定為tomcat修改檔案不用重啟就自動重新裝載類庫而驚歎吧。
由於篇幅所限,本文集中探討第一個和第三個原因,即tomcat中如何利用classloader做到部分隔離,部分共享的,以及tomcat如何做到熱部署的。
首先,我們討論tomcat中如何做到lib的部分隔離,部分共享的。在Bootstrap中,可以找到如下程式碼:
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) { log.error("Class loader creation threw exception", t); System.exit(1); } }
應該可以看出來,這裡建立了3個classloader,分別是common,server和shared,並且common是server和shared之父。如果感興趣,可以看下createClassLoader,它會呼叫進ClassLoaderFactory.createClassLoader,這個工廠方法最後會建立一個StandardClassLoader,StandardClassLoader僅僅繼承了URLClassLoader而沒有其他更多改動,也就是說上面3個classloader都是StandardClassLoader,除了層次關係之外,他們與jvm定義的classloader並沒有區別,這就意味著他們同樣遵循雙親委派模型,只要我們能夠用它裝載指定的類,則它就自然的嵌入到了jvm的classloader體系中去了。Tomcat的classloader體系如圖:
問題來了,tomcat是如何將自己和webapp的所有類用自己的classloader載入的呢?是否需要有個專門的地方遍歷所有的類並將其載入,可是程式碼裡並不能找到這樣的地方。而且相對來說,將不用的類顯式的載入進來也是一種浪費,那麼,tomcat(或者說jvm)是如何做到這點呢?
這裡有個隱式載入的問題,所謂的隱式載入,就是指在當前類中所有new的物件,如果沒有被載入,則使用當前類的類載入器載入,即this.getClass(),getClassLoader()會預設載入該類中所有被new出來的物件的類(前提是他們沒在別處先被載入過)。從這裡思考,我們一個一個的應用,本質上是什麼樣子,事實上,正如所有程式都有一個main函式一樣,所有的應用都有一個或多個startup的類(即入口),這個類是被最先載入的,並且隨後的所有類都像樹枝一樣以此類為根被載入,只要控制了載入該入口的classloader,等於就控制了所有其他相關類的classloader。
以此為線索來看tomcat的Bootstrap中的init程式碼
public void init()
throws Exception
{
// Set Catalina path
setCatalinaHome();
setCatalinaBase();
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
在catalinaLoader.loadClass之後,Catalina事實上就由server這個classloader載入進來了,而下一句newInstance時,所有以Catalina為根的物件的類也會全部被隱式載入進來,但是為什麼這裡需要在其後費盡筆墨反射去setParentClassLoader呢,直接用((Catalina)startupInstance).setParentClassLoader豈不是更加方便?要注意,如果這樣寫,這個強制轉換的Catalina便會由載入BootStrap的classloader(URLClassLoader)載入進來,而startupInstance是由StandardClassLoader載入進來的,並不是一個class,由此會拋一個ClassCastException。這也是類庫可能發生衝突的一個原因。
有同學問到為什麼在eclipse中除錯tomcat原始碼時把反射換成((Catalina)startupInstance).setParentClassLoader是完全合法的,沒有報任何異常。這裡需要注意tomcat的啟動預設會把bin下的bootstrap.jar加入classpath:set "CLASSPATH=%CLASSPATH%%CATALINA_BASE%\bin\tomcat-juli.jar;%CATALINA_HOME%\bin\bootstrap.jar",而eclipse中除錯tomcat是所有相關類都在classpath的,區別在於,第一種情況,雙親委派模型在上層找不到Catalina.class,則StandardClassLoader去lib下載入catalina.jar;而第二種情況,AppClassLoader直接能夠找到Catalina.class,所以就由他載入了,StandardClassLoader就形同虛設了。所以我們不能單從現象去判斷原因,這也是我們為什麼要學習classloader載入原理的原因。
搞明白這點,其實就可以理解tomcat是如何使用自己的classloader載入類進來並且如何隔離server和shared類的載入了。
但是另一個問題,tomcat又是如何隔離不同的webapp的載入呢?
對於每個webapp應用,都會對應唯一的StandContext,在StandContext中會引用WebappLoader,該類又會引用WebappClassLoader,WebappClassLoader就是真正載入webapp的classloader。
StandContext隸屬於Lifecycle管理,在start方法中會做一系列準備工作(有興趣可以參考,實際上該方法比較重要,但是篇幅太長),比如新建WebappClassLoader,另外loadOnStartup便會載入所有配置好的servlet(每個StandardWrapper負責管理一個servlet),這裡同樣的一個問題是,在我們自己寫的web應用程式中,入口是什麼?答案就是Servlet, Listener, Filter這些元件,如果我們控制好入口的classloader,便等於控制了其後所載入的全部類,那麼,tomcat是如何控制的呢?且看StandardWrapper中一個重要的方法loadServlet(篇幅所限,隱去了大部分不想關內容),getLoader()事實上呼叫到了StandContext中儲存的WebappLoader,於是,用該loader載入Servlet,從而控制住了Servlet中所有待載入的類。
public synchronized Servlet loadServlet() throws ServletException {
...
Servlet servlet;
try {
...
// Acquire an instance of the class loader to be used
Loader loader = getLoader();
if (loader == null) {
unavailable(null);
throw new ServletException
(sm.getString("standardWrapper.missingLoader", getName()));
}
ClassLoader classLoader = loader.getClassLoader();
// Special case class loader for a container provided servlet
//
if (isContainerProvidedServlet(actualClass) &&
! ((Context)getParent()).getPrivileged() ) {
// If it is a priviledged context - using its own
// class loader will work, since it's a child of the container
// loader
classLoader = this.getClass().getClassLoader();
}
// Load the specified servlet class from the appropriate class loader
Class classClass = null;
try {
if (SecurityUtil.isPackageProtectionEnabled()){
...
} else {
if (classLoader != null) {
classClass = classLoader.loadClass(actualClass);
} else {
classClass = Class.forName(actualClass);
}
}
} catch (ClassNotFoundException e) {
unavailable(null);
getServletContext().log( "Error loading " + classLoader + " " + actualClass, e );
throw new ServletException
(sm.getString("standardWrapper.missingClass", actualClass),
e);
}
if (classClass == null) {
unavailable(null);
throw new ServletException
(sm.getString("standardWrapper.missingClass", actualClass));
}
// Instantiate and initialize an instance of the servlet class itself
try {
servlet = (Servlet) classClass.newInstance();
// Annotation processing
if (!((Context) getParent()).getIgnoreAnnotations()) {
if (getParent() instanceof StandardContext) {
((StandardContext)getParent()).getAnnotationProcessor().processAnnotations(servlet);
((StandardContext)getParent()).getAnnotationProcessor().postConstruct(servlet);
}
}
} catch (ClassCastException e) {
...
} catch (Throwable e) {
...
}
...
return servlet;
}
這裡的載入過程與之前的一致,至於如何做到不同webapp之間的隔離,我想大家已經明白,不同的StandardContext有不同的WebappClassLoader,那麼不同的webapp的類裝載器就是不一致的。裝載器的不一致帶來了名稱空間不一致,所以webapp之間是相互隔離的。
關於tomcat是如何做到熱部署的,相信不用說也能猜到個十之八九。簡單講就是定期檢查是否需要熱部署,如果需要,則將類裝載器也重新裝載,並且去重新裝載其他相關類。關於tomcat是如何做的,可以具體看以下分析。
首先來看一個後臺的定期檢查,該定期檢查是StandardContext的一個後臺執行緒,會做reload的check,過期session清理等等,這裡的modified實際上呼叫了WebappClassLoader中的方法以判斷這個class是不是已經修改。注意到他呼叫了StandardContext的reload方法。
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (container instanceof StandardContext) {
((StandardContext) container).reload();
}
} finally {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
}
} else {
closeJARs(false);
}
}
那麼reload方法具體做了什麼?非常簡單,就是tomcat lifecycle中標準的啟停方法stop和start,別忘了,start方法會重新造一個WebappClassLoader並且重複loadOnStartup的過程,從而重新載入了webapp中的類,注意到一般應用很大時,熱部署通常會報outofmemory: permgen space not enough之類的,這是由於之前載入進來的class還沒有清除而方法區記憶體又不夠的原因
public synchronized void reload() {
// Validate our current component state
if (!started)
throw new IllegalStateException
(sm.getString("containerBase.notStarted", logName()));
// Make sure reloading is enabled
// if (!reloadable)
// throw new IllegalStateException
// (sm.getString("standardContext.notReloadable"));
if(log.isInfoEnabled())
log.info(sm.getString("standardContext.reloadingStarted",
getName()));
// Stop accepting requests temporarily
setPaused(true);
try {
stop();
} catch (LifecycleException e) {
log.error(sm.getString("standardContext.stoppingContext",
getName()), e);
}
try {
start();
} catch (LifecycleException e) {
log.error(sm.getString("standardContext.startingContext",
getName()), e);
}
setPaused(false);
}
參考資料:http://blog.csdn.net/aesop_wubo/article/details/7582266
http://my.oschina.net/321tiantian/blog/69399
http://developer.51cto.com/art/201003/185704.htm