JVM学习笔记03-打破双亲委派机制(Tomcat)

3,074 阅读4分钟

Tomcat为何要打破双亲委派机制

开始之前,我们有个问题可以探讨一下:Tomcat使用默认的双亲委派类加载机制是否可行?

首先,我们可以思考一下,Tomcat作为一个web容器,它需要解决什么问题?

  1. 假如有若干个应用程序部署在Tomcat上,这些应用程序可能会依赖到同一第三方类库的不同版本,因此Tomcat必须支持每个应用程序的类库可以相互隔离
  2. 部署在同一个Tomcat上的不同应用程序,相同类库的相同版本应该是共享的,否则就会出现大量相同的类加载到虚拟机中
  3. Tomcat本身也有依赖的类库,与应用程序依赖的类库可能会混淆,基于安全考虑,应该将两者进行隔离
  4. 要支持Jsp文件修改后,其生成的class能在不重启的情况下及时被加载进JVM

那么,采用默认的双亲委派类加载机制,能否解决上述问题呢?

  • 问题1、3,如果Tomcat采用默认的双亲委派加载机制,是无法加载同一类库不同版本的类的,因为默认的双亲委派加载机制在加载类时,是通过类的全限定名做唯一性校验的
  • 问题2,默认的双亲委派类加载机制可以实现,因为它本就能保证唯一性
  • 问题4,我们知道Jsp文件更新其实也就是class文件更新了,此时类的全限定名并没有改变,修改Jsp文件后,类加载器会从方法区中直接取到已存在的,这会导致修改后Jsp文件其实不会重新加载。那么,如果直接卸载掉这个Jsp文件的类加载器,再重新创建类加载器去加载修改后的Jsp文件,不就能解决问题了吗?那么你应该能猜到每个Jsp文件应对应一个唯一的类加载器

到此,我们可以得出答案,Tomcat只使用默认的双亲委派类加载机制是不可行的

Tomcat中的自定义类加载器

Tomcat的自定义类加载器 (1).png

Tomcat的几个主要的自定义类加载器

  • CommonClassLoader:公共的类加载器,其加载的class可以被Tomcat容器本身以及各个Webapp访问
  • CatalinaClassLoader:私有的类加载器,其加载的class对于Webapp不可见
  • ShareClassLoader:各个Webapp共享的类加载器,其加载的class对于所有Webapp可见,但对于Tomcat容器本身不可见
  • WebappClassLoader:各个Webapp私有的类加载器,其加载的class只对当前的Webapp可见

从上面的图,不难看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoaderShareClassLoader使用,从而实现公有类库的公用,而CatalinaClassLoaderShareClassLoader各自加载的类则与对方相互隔离
  • WebappClassLoader可以使用ShareClassLoader加载的类,但各个WebappClassLoader之间相互隔离
  • JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那个.class文件,一对一的设计是为了随时丢弃它,当Tomcat检测到JSP文件被修改时,会替换掉当前的JasperLoader的实例,并通过再一次建立一个新的JasperLoader实例来实现JSP文件的热加载功能

由此可知,Tomcat的设计是违背Java的双亲委派模型的,每个WebappClassLoader加载自己目录下的.class文件,不会传递给父加载器,这就打破了双亲委派机制,这样做正是为了实现隔离性。

下面这段代码模拟实现了Tomcat的WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

package org.laugen.jvm;
import sun.misc.PerfCounter;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class TestCustomizeClassLoader {
    static class CustomizeClassLoader extends ClassLoader {
        private String classPath;

        public CustomizeClassLoader(String classPath) {
            this.classPath = classPath;
        }

        // 读取class字节码文件
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t1 = System.nanoTime();
                    if (name.startsWith("org.laugen.jvm.Note")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(name);
                    }
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }

    }

    public static void main(String args[]) throws Exception {
        CustomizeClassLoader classLoader1 = new CustomizeClassLoader("D:/MyClasses-v1");
        System.out.println("自定义类加载器的父加载器:" + classLoader1.getParent().getClass().getName());
        Class clazz1 = classLoader1.loadClass("org.laugen.jvm.Note");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("print", null);
        method1.invoke(obj1, null);
        System.out.println("Note类的类加载器是:" + clazz1.getClassLoader().getClass().getName());
        
        System.out.println("====================================================================");
        
        CustomizeClassLoader classLoader2 = new CustomizeClassLoader("D:/MyClasses-v2");
        System.out.println("自定义类加载器的父加载器:" + classLoader2.getParent().getClass().getName());
        Class clazz2 = classLoader2.loadClass("org.laugen.jvm.Note");
        Object obj2 = clazz2.newInstance();
        Method method2 = clazz2.getDeclaredMethod("print", null);
        method2.invoke(obj2, null);
        System.out.println("Note类的类加载器是:" + clazz2.getClassLoader().getClass().getName());
    }
}

运行结果如下:

自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V1版本)
创建了org.laugen.jvm.Note类的实例(V1版本)
这是一个note(V1版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader
====================================================================
自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V2版本)
创建了org.laugen.jvm.Note类的实例(V2版本)
这是一个note(V2版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader

由此可知,在同一个JVM中,两个相同全限定名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个时,出了要看类的全限定名外,还需要看他们的类加载器是不是同一个

那么,你知道Tomcat的JasperLoader热加载是怎么实现的吗?