浅议tomcat与classloader

   关于tomcat和classloader的文章,网上多如牛毛,且互相转载,因此大多数搜到的基本上是讲到了tomcat中classloader的几个层次,对于初接触classloader,看了以后仍是只知其然不知其因此然。java

  一直比较好奇,为何tomcat须要实现本身的classloader,jvm提供的classloader有什么不符合须要?web

  事实上,tomcat之因此造了一堆本身的classloader,大体是出于下面三类目的:apache

  • 对于各个webapp中的class和lib,须要相互隔离,不能出现一个应用中加载的类库会影响另外一个应用的状况;而对于许多应用,须要有共享的lib以便不浪费资源,举个例子,若是webapp1和webapp2都用到了log4j,能够将log4j提到tomcat/lib中,表示全部应用共享此类库,试想若是log4j很大,而且20个应用都分别加载,那实在是没有必要的。
  • 第二个缘由则是与jvm同样的安全性问题。使用单独的classloader去装载tomcat自身的类库,以避免其余恶意或无心的破坏;
  • 第三个缘由是热部署的问题。相信你们必定为tomcat修改文件不用重启就自动从新装载类库而惊叹吧。

   本文集中探讨第一个和第三个缘由,即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

相关文章
相关标签/搜索