關於tomcat和classloader的文章,網上多如牛毛,且互相轉載,因此大多數搜到的基本上是講到了tomcat中classloader的幾個層次,對於初接觸classloader,看了以後仍是隻知其然不知其因此然。java
一直比較好奇,爲何tomcat須要實現本身的classloader,jvm提供的classloader有什麼不符合須要?web
事實上,tomcat之因此造了一堆本身的classloader,大體是出於下面三類目的:apache
本文集中探討第一個和第三個緣由,即tomcat中如何利用classloader作到部分隔離,部分共享的,以及tomcat如何作到熱部署的。bootstrap
首先,咱們討論tomcat中如何作到lib的部分隔離,部分共享的。在Bootstrap中,能夠找到以下代碼:tomcat
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)是如何作到這點呢?session
這裏有個隱式加載的問題,所謂的隱式加載,就是指在當前類中全部new的對象,若是沒有被加載,則使用當前類的類加載器加載,即this.getClass(),getClassLoader()會默認加載該類中全部被new出來的對象的類(前提是他們沒在別處先被加載過)。從這裏思考,咱們一個一個的應用,本質上是什麼樣子,事實上,正如全部程序都有一個main函數同樣,全部的應用都有一個或多個startup的類(即入口),這個類是被最早加載的,而且隨後的全部類都像樹枝同樣以此類爲根被加載,只要控制了加載該入口的classloader,等於就控制了全部其餘相關類的classloader。app
以此爲線索來看tomcat的Bootstrap中的init代碼:eclipse
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。這也是類庫可能發生衝突的一個緣由。webapp
有同窗問到爲何在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/liweisnake/article/details/8470285