tomcat学习之四:tomcat的类加载机制

来源:互联网 发布:vue.js nexttick用法 编辑:程序博客网 时间:2024/05/29 13:49


        tomcat的类加载机制遵循了java类加载机制中经典的双亲委派模型。所以要了解tomcat的类加载机制需要先了解双亲委派模型。

        在程序中用到的类需要由类加载器将类的class文件加载到内存中,然后经由JVM验证、解析、初始化后才能使用,如下段代码:

public static void main(String[] args) throws Exception {User user = new User();user.setUsername("tom");user.setPhone("110");user.setSex("man");System.out.println(user);}
        User类需要类加载器将User.class加载到内存中,然后经过上述所说的步骤后,程序才能使用,类加载器在Java中使用ClassLoader对象表示。一个类在程序运行期间只会被加载一次,而类加载器会在加载后创建该类的class对象,会将class对象保存在方法区,也称永久代(Metaspace)的内存区域,并在将该class对象的引用放到Vector中。 ClassLoader中相关代码如下:

// Invoked by the VM to record every loaded class with this loader.void addClass(Class<?> c) {    classes.addElement(c);}
         由于类加载机制有一个非常重要的特性,那就是类加载器A加载的类不能被类加载器B加载的类使用,具体可见下面代码:

//定义自己的类加载器ClassLoader cl = new ClassLoader() {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {try {//违背双亲委派模型,先自己加载,自己不能加载再交给父加载器加载String str = name.substring(name.lastIndexOf(".") + 1) + ".class";InputStream is = getClass().getResourceAsStream(str);if (is == null) {return super.loadClass(name);}byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}};Object obj = cl.loadClass("cn.yamikaze.java.basic.HelloJava").newInstance();System.out.println(obj.getClass()); // Class cn.yamikaze.java.basic.HelloJava// 结果为false// 为什么? 因为obj是自定义的类加载器加载产生的对象,而HelloJava是由系统加载器加载的System.out.println(obj instanceof cn.yamikaze.java.basic.HelloJava);
         所以为了保证一个类只由一个类加载器加载,所以引入了双亲委派模型。什么是双亲委派模型? 每一个加载器都要通过组合的方式保存一个加载器的实例作为父加载器。当使用当前类加载器加载类时,当前类加载器不会自己加载,会先交给父加载器加载,父加载器加载时会重复这个过程,直到到达顶端的类加载器后。如果还不能加载就抛出ClassNotFoundException,子加载器捕获到异常,就会尝试自己加载,如果自己加载不了,重复这个过程,直到最顶端的加载器,如果这时候最低端的加载器也不能加载,就会抛出ClassNotFoundException。而且明确规范了,如果一个类加载器第一次能加载成功,那么以后的加载必须成功,如果第一次失败,那么以后的加载也必须失败。这就保证了一个类只由一个类加载器加载。同时 通过类加载器的分层,也将java类库分了层。例如java.lang.Object作为一个基类,只能由Bootstrap类加载器加载(最顶层的类加载器)。

         至于为什么java不采用单一加载器的原因,我想可能是那样做职责不明确吧!但具体什么原因我也不知,如果知道的小伙伴请在下面留言谢谢!

1、双亲委派模型

          java的双亲委派模型如下图所示(组合方式实现继承关系):

         Bootstrap类加载器:负责加载jre/lib下的jar,例如rt.jar是java的核心类库,Object、String、System等常用类都存在于rt.jar中。这个加载器存在于虚拟机中,如果是HotSpot虚拟机,这个类加载器是C++语言编写,如果虚拟机本身就是由纯java语言实现,那就是由java编写。如果想要使用这个加载器,可以使用参数-Xbootclasspath指定加载路径,但boostrap类加载器只会加载虚拟机识别的类库,例如rt.jar,否则即使名字不符合,放在lib中也不会被加载。

         Extension类加载器:扩展类加载器,负责加载jre/lib/ext中的jar,或者java.ext.dirs系统变量指定路径的类库。

         Application类加载器:负责加载用户类路径上指定的类库,由于是ClassLoader的getSystemClassLoader方法的返回值,所以也叫作系统类加载器。

2、tomcat的类加载器

2.1、为什么tomcat要实现自己的类加载器?

          tomcat作为一个服务器,在它上面可以部署多个应用。默认情况下是使用1中的应用加载器加载类的,但往往部署的应用都会有多个jar依赖,所以第一点为了解决依赖问题,必须保证每个应用的类库独立。 为什么要保证每个应用的类库相互独立呢? 打个比方,我们都知道连接数据库需要数据库驱动,而数据库驱动的版本要与连接的数据库相对应,否则无法获取连接。 那假设部署在tomcat上的两个应用依赖的数据库驱动版本有很大差异,这样直接使用java的系统类加载器会导致这两个应用有一个无法启动。
      
           a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
           b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
           c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
           d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。
          
           由于存在上述问题,tomcat实现了自己的类加载器,不仅仅是tomcat,所有的服务器基本都有或多或少上述所说的问题。

2.2、tomcat的类加载器设计

           tomcat的类加载器设计如下图所示:


         ps: 其中蓝色的类加载器为tomcat自己实现的类加载器。
         Common类加载器:负责加载/common目录的类库,这儿存放的类库可被tomcat以及所有的应用使用。

         Catalina类加载器:负责加载/server目录的类库,只能被tomcat使用。

         Shared类加载器:负载加载/shared目录的类库,可被所有的web应用使用,但tomcat不可使用。

         WebApp类加载器:负载加载单个Web应用下classes目录以及lib目录的类库,只能当前应用使用。

         Jsp类加载器:负责加载Jsp,每一个Jsp文件都对应一个Jsp加载器。

         Tomcat运行期间,Webapp类加载器与Jsp类加载器个数为复数。通过上图的设计,可以解决掉2.1中的问题。

         可能看到这儿你会翻出tomcat目录结构查看一下,然后你会发现根本没有common、shared、server目录。这是因为只有在conf目录下的catalina.properties指定了server.loader 以及 share.loader两个属性tomcat才会建立CatalinaClassLoader和SharedClassLoader实例,而默认情况下都没指定,所以CatalinaClassLoader以及SharedClassLoader都会使用CommonClassLoader来代替,所以tomcat6.x以上顺理成章地把上述三个目录合并成了一个lib目录。这个目录相当于/common目录的作用。

来看看源码中关于这三个类加载器的创建吧,如下代码:

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) {        handleThrowable(t);        log.error("Class loader creation threw exception", t);        System.exit(1);    }}private ClassLoader createClassLoader(String name, ClassLoader parent)        throws Exception {    String value = CatalinaProperties.getProperty(name + ".loader");    //默认情况下为空,直接返回parent,这儿的parent就是CommonLoader    if ((value == null) || (value.equals("")))        return parent;    value = replace(value);    List<Repository> repositories = new ArrayList<Repository>();    StringTokenizer tokenizer = new StringTokenizer(value, ",");    //这儿是对类加载器定义加载的路径解析    while (tokenizer.hasMoreElements()) {        String repository = tokenizer.nextToken().trim();        if (repository.length() == 0) {            continue;        }        // 不是本地类库        try {            @SuppressWarnings("unused")            URL url = new URL(repository);            repositories.add(                    new Repository(repository, RepositoryType.URL));            continue;        } catch (MalformedURLException e) {            // Ignore        }        // Local repository        if (repository.endsWith("*.jar")) {            repository = repository.substring                (0, repository.length() - "*.jar".length());            repositories.add(                    new Repository(repository, RepositoryType.GLOB));        } else if (repository.endsWith(".jar")) {            repositories.add(                    new Repository(repository, RepositoryType.JAR));        } else {            repositories.add(                    new Repository(repository, RepositoryType.DIR));        }    }    //使用工厂创建    return ClassLoaderFactory.createClassLoader(repositories, parent);}
      可以看到initClassLoader方法中的调用,再结合catalina.properties文件,CommonClassLoader、ServerClassLoader、ShareClassLoader为同一个类加载器CommonClassLoader。上述代码存在于org.apache.catalina.startup.Bootstrap类中。

       创建完成后会将ShareClassLoader通过反射调用Catalina类setParentClassLoader方法设置到parentClassLoader属性中,然后在解析方法中再设置到Engine容器中去,部分代码如下:

// Add RuleSets for nested elementsdigester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));digester.addRuleSet(new EngineRuleSet("Server/Service/"));digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));// When the 'engine' is found, set the parentClassLoader.digester.addRule("Server/Service/Engine",                new SetParentClassLoaderRule(parentClassLoader));

 2.3、Tomcat类加载器的实现

         java中用户自定义类加载器需要实现ClassLoader类,ClassLoader类结构如下:

         在tomcat中,类加载器实现是通过继承URLClassLoader类来实现的,URLClassLoader是ClassLoader的一个子类。 下面是WebappClassLoaderBase部分源码:

protected String[] repositories = new String[0];protected URL[] repositoryURLs = null;/** * Repositories translated as path in the work directory (for Jasper * originally), but which is used to generate fake URLs should getURLs be * called. */protected File[] files = new File[0];/** * The list of JARs, in the order they should be searched * for locally loaded classes or resources. */protected JarFile[] jarFiles = new JarFile[0];/** * The list of JARs, in the order they should be searched * for locally loaded classes or resources. */protected File[] jarRealFiles = new File[0];/** * The path which will be monitored for added Jar files. */protected String jarPath = null;/** * The list of JARs, in the order they should be searched * for locally loaded classes or resources. */protected String[] jarNames = new String[0];
       这个ClassLoader自然就是对应一个Web应用,上面代码表示这个ClassLoader的加载范围。ClassLoader是在StandardContext的startInternal方法中与Context绑定的。以下为StandardWrapper获取Servlet实例代码:

public synchronized Servlet loadServlet() throws ServletException {        if (unloading) {            throw new ServletException(                    sm.getString("standardWrapper.unloading", getName()));        }        // Nothing to do if we already have an instance or an instance pool        if (!singleThreadModel && (instance != null))            return instance;        PrintStream out = System.out;        if (swallowOutput) {            SystemLogHandler.startCapture();        }        Servlet servlet;        try {            long t1=System.currentTimeMillis();            // Complain if no servlet class has been specified            if (servletClass == null) {                unavailable(null);                throw new ServletException                    (sm.getString("standardWrapper.notClass", getName()));            }            /**             * 交给父容器(Context)加载,父容器再交给InstanceManager加载。             * InstanceManager中的ClassLoader是在StandardContext的startInternal方法设置进去的             */            InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();            try {                servlet = (Servlet) instanceManager.newInstance(servletClass);            } catch (ClassCastException e) {                unavailable(null);                // Restore the context ClassLoader                throw new ServletException                    (sm.getString("standardWrapper.notServlet", servletClass), e);            } catch (Throwable e) {                e = ExceptionUtils.unwrapInvocationTargetException(e);                ExceptionUtils.handleThrowable(e);                unavailable(null);                // Added extra log statement for Bugzilla 36630:                // http://bz.apache.org/bugzilla/show_bug.cgi?id=36630                if(log.isDebugEnabled()) {                    log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);                }                // Restore the context ClassLoader                throw new ServletException                    (sm.getString("standardWrapper.instantiate", servletClass), e);            }            if (multipartConfigElement == null) {                MultipartConfig annotation =                        servlet.getClass().getAnnotation(MultipartConfig.class);                if (annotation != null) {                    multipartConfigElement =                            new MultipartConfigElement(annotation);                }            }            processServletSecurityAnnotation(servlet.getClass());            // Special handling for ContainerServlet instances            if ((servlet instanceof ContainerServlet) &&                    (isContainerProvidedServlet(servletClass) ||                            ((Context) getParent()).getPrivileged() )) {                ((ContainerServlet) servlet).setWrapper(this);            }            classLoadTime=(int) (System.currentTimeMillis() -t1);            if (servlet instanceof SingleThreadModel) {                if (instancePool == null) {                    instancePool = new Stack<Servlet>();                }                singleThreadModel = true;            }            initServlet(servlet);            fireContainerEvent("load", this);            loadTime=System.currentTimeMillis() -t1;        } finally {            if (swallowOutput) {                String log = SystemLogHandler.stopCapture();                if (log != null && log.length() > 0) {                    if (getServletContext() != null) {                        getServletContext().log(log);                    } else {                        out.println(log);                    }                }            }        }        return servlet;    }
        可以看到注释部分,加载实例交给了父容器StandardContext完成。

        对于jsp类加载器,每一个jsp文件都对应了一个jsp类加载器。 在tomcat中,jsp也是一个servlet, 也会被StandardWrapper包装。那jsp文件修改后可以直接将改变展示在浏览器中,无须重启tomcat,也无需reload容器,那这是怎么做的呢?

        对于容器,在ContainerBase类的startInternal方法中,会调用这段代码:

protected void threadStart() {    if (thread != null)        return;    if (backgroundProcessorDelay <= 0)        return;    threadDone = false;    String threadName = "ContainerBackgroundProcessor[" + toString() + "]";    thread = new Thread(new ContainerBackgroundProcessor(), threadName);    thread.setDaemon(true);    thread.start();}
        可以很明显看出,启动了一个后台线程,后台线程代码如下:

public void run() {            Throwable t = null;            String unexpectedDeathMessage = sm.getString(                    "containerBase.backgroundProcess.unexpectedThreadDeath",                    Thread.currentThread().getName());            try {                while (!threadDone) {                    try {                        Thread.sleep(backgroundProcessorDelay * 1000L);                    } catch (InterruptedException e) {                        // Ignore                    }                    if (!threadDone) {                        Container parent = (Container) getMappingObject();                        ClassLoader cl =                             Thread.currentThread().getContextClassLoader();                        if (parent.getLoader() != null) {                            cl = parent.getLoader().getClassLoader();                        }                        processChildren(parent, cl);//关键是这句                    }                }            } catch (RuntimeException e) {                t = e;                throw e;            } catch (Error e) {                t = e;                throw e;            } finally {                if (!threadDone) {                    log.error(unexpectedDeathMessage, t);                }            }        }        protected void processChildren(Container container, ClassLoader cl) {            try {                if (container.getLoader() != null) {                    Thread.currentThread().setContextClassLoader                        (container.getLoader().getClassLoader());                }                container.backgroundProcess();            } catch (Throwable t) {                ExceptionUtils.handleThrowable(t);                log.error("Exception invoking periodic operation: ", t);            } finally {                Thread.currentThread().setContextClassLoader(cl);            }            Container[] children = container.findChildren();            for (int i = 0; i < children.length; i++) {                if (children[i].getBackgroundProcessorDelay() <= 0) {                    processChildren(children[i], cl);                }            }}
             可以看到processChildren调用了容器的backgroundProcess方法并且调用了子容器的这个方法,继续看backgroundProcess里面干了些什么:

              ps:这个方法在ContainerBase有默认实现,但只有StandardContext以及StandardWrapper方法,这个默认实现主要是为StandardContext准备的,它检查java类文件是否有所改变,然后调用reload方法重启容器,或者为容器设置ClassLoader。 

              下面是StandardWrapper的backgroundProcess方法:

/** * Execute a periodic task, such as reloading, etc. This method will be * invoked inside the classloading context of this container. Unexpected * throwables will be caught and logged. */@Overridepublic void backgroundProcess() {    super.backgroundProcess();            if (!getState().isAvailable())        return;            if (getServlet() != null && (getServlet() instanceof PeriodicEventListener)) {        ((PeriodicEventListener) getServlet()).periodicEvent();    }}
              这个方法没什么特别的,主要是为Servlet类型为JspServlet的StandardWrapper准备的,因为JspServlet实现了PeriodicEventListener。下面是调用的该方法:

@Overridepublic void periodicEvent() {    rctxt.checkUnload();    rctxt.checkCompile();}
           checkCompile方法主要是调用JspCompiler重新编译Jsp文件(丢掉当前jsp类加载器,重新创建一个),前提是修改时间 + 超时时间 小于当前时间才会重新编译,当然在tomcat7中,超时时间默认为0,且无法设置。其实这个方法并不是直接由servlet的rctxt属性来调用,中间还会委托给其他对象,由其他对象来完成调用,但由于篇幅有限,且这也不是本篇讨论的重点。

           以上就是我对tomcat类加载机制的学习与理解,如果不足或错误,欢迎在下面评论指正。最后,感谢你的阅读! ( ̄▽ ̄)~*









         


原创粉丝点击