zoukankan      html  css  js  c++  java
  • 浅议tomcat与classloader

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

      一直比较好奇,为什么tomcat需要实现自己的classloader,jvm提供的classloader有什么不符合需要?

      事实上,tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:

    • 对于各个webapp中的class和lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况;而对于许多应用,需要有共享的lib以便不浪费资源,举个例子,如果webapp1和webapp2都用到了log4j,可以将log4j提到tomcat/lib中,表示所有应用共享此类库,试想如果log4j很大,并且20个应用都分别加载,那实在是没有必要的。
    • 第二个原因则是与jvm一样的安全性问题。使用单独的classloader去装载tomcat自身的类库,以免其他恶意或无意的破坏;
    • 第三个原因是热部署的问题。相信大家一定为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%in omcat-juli.jar;%CATALINA_HOME%inootstrap.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

  • 相关阅读:
    【cocos2d-js官方文档】二、资源管理器Assets Manager
    【cocos2d-js官方文档】七、CCFileUtils
    【cocos2d-js官方文档】九、cc.loader
    【cocos2d-js官方文档】十二、对象缓冲池
    【cocos2d-js官方文档】十二、对象缓冲池
    【cocos2d-js官方文档】二十一、v3相对于v2版本的api变动
    【cocos2d-js教程】cocos2d-js 遮挡层(禁止触摸事件传递层)
    cocos2d-js中怎么删除一个精灵
    MS Chart 折线图——去除时间中的时、分、秒,按天统计【转】
    MS Chart 条状图【转】
  • 原文地址:https://www.cnblogs.com/moonandstar08/p/5918226.html
Copyright © 2011-2022 走看看