java与tomcat7类加载机制

1. java类加载器

近来了解tomcat的类加载机制,所以先回顾一下java虚拟机类加载器,如果从java虚拟机的角度来看的话,其实类加载器只分为两种:一种是启动类加载器(即Bootstrap ClassLoader),通过使用JNI来实现,我们无法获取到到它的实例;另一种则是java语言实现java.lang.ClassLoader的子类。一般从我们的角度来看,会根据类加载路径会把类加载器分为3种:Bootstrap ClassLoader,ExtClassLoader,AppClassLoader.后两者是sun.misc.Launcher类的内部类,而前者在JDK源码中是没有与之对应的类的,倒是在sun.misc.Launcher中可以看到一些它的加载路径信息。如果找不到sun的源码,可以下载OpenJDK的来看一下。

Bootstrap ClassLoader: 引导类加载器,从%JAVA_RUNTIME_JRE%/lib目录加载,但并不是将该目录所有的类库都加载,它会加载一些符合文件名称的,例如:rt.jar,resources.jar等。在sun.misc.Launcher源码中也可以看得它的加载路径:

private static String bootClassPath = System.getProperty("sun.boot.class.path");

或者配置-Xbootclasspath参数指定加载的路径,通过获取环境变量sun.boot.class.path看一下到底具体加载了那些类:

D:\Program Files\Java\jdk1.7.0_67\jre\lib\resources.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\rt.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jce.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.7.0_67\jre\classes

Extension ClassLoader:扩展类加载器,实现类为sun.misc.Launcher$ExtClassLoader,加载%JAVA_RUNTIME_JRE%/lib/ext/目录下的jar包,也可以在sun.misc.Launcher源码中也可以看得它的加载路径:

String s = System.getProperty("java.ext.dirs");

通过获取java.ext.dirs环境变量打印一下:

D:\Program Files\Java\jdk1.7.0_67\jre\lib\ext

Appication ClassLoader:应用程序类加载器,或者叫系统类加载器,实现类为sun.misc.Launcher$AppClassLoader。从sun.misc.Launcher的构造函数中可以看到,当AppClassLoader被初始化以后,它会被设置为当前线程的上下文类加载器以及保存到Launcher类的loader属性中,而通过ClassLoader.getSystemClassLoader()获取的也正是该类加载器(Launcher.loader)。应用类加载器从用户类路径中加载类库,可以在源码中看到:

final String s = System.getProperty("java.class.path");

1.1 类关系

classloader-1

由图看到Bootstrap ClassLoader并不在继承链上,因为它是java虚拟机内置的类加载器,对外不可见。可以看到顶层ClassLoader有一个parent属性,用来表示着类加载器之间的层次关系(双亲委派模型);注意,ExtClassLoader类在初始化时显式指定了parent为null,所以它的父类加载器默认为Bootstrap ClassLoader。在tomcat中都是通过扩展URLClassLoader来实现自己的类加载器。

1.2 双亲委托模型

这3种类加载器之间存在着父子关系(区别于java里的继承),子加载器保存着父加载器的引用。当一个类加载器需要加载一个目标类时,会先委托父加载器去加载,然后父加载器会在自己的加载路径中搜索目标类,父加载器在自己的加载范围中找不到时,才会交还给子加载器加载目标类。

采用双亲委托模式可以避免类加载混乱,而且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中一样的类,最终还是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,所以两者都不会再重新加载。当然,如果使用者通过自定义的类加载器可以强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException异常。

classloader-2

  1. 启动类加载器是扩展类加载器的父类加载器:扩展类加载器在sun.misc.Launcher构造函数中被初始化,它的父类加载器被设置了为null,那为什么还说启动类加载器是它的父加载器?看一下ClassLoader.loadClass()方法:
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,查找该类是否已经被加载过了
            Class c = findLoadedClass(name);
            if (c == null) {  //未被加载过
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {  // 父类加载器不为null,则调用父类加载器尝试加载
                        c = parent.loadClass(name, false);
                    } else {   // 父类加载器为null,则调用本地方法,交由启动类加载器加载,所以说ExtClassLoader的父类加载器为Bootstrap ClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) { //仍然加载不到,只能由本加载器通过findClass去加载
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

从代码中看到,如果parent==null,将会由启动类加载器尝试加载,所以扩展类加载器的父类加载器是启动类加载器。

  1. 扩展类加载器是应用程序类加载器的父类加载器:这个比较好理解,依然是在sun.misc.Launcher构造函数初始化应用程序类加载器时,指定了ExtClassLoader为AppClassLoader的父类加载器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的属性,extcl是扩展类加载器实例
  1. 应用程序类加载器是自定义类加载器的父类加载器:这里指的是使用默认构造函数进行自定义类加载器(否则 你可以指定parent来构造一个父加载器为ExtClassLoader的自定义类加载器),无论是通过扩展ClassLoader还是URLClassLoader最终都会获取系统类加载器(AppClassLoader)作为父类加载器:
protected ClassLoader() {
        //调用getSystemClassLoader方法获取系统类加载器作为父类加载器
        this(checkCreateClassLoader(), getSystemClassLoader()); 
    }
public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader(); //初始化系统类加载器
        .....
        return scl;
    }
private static synchronized void initSystemClassLoader() {
        ......
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        ......
        scl = l.getClassLoader();  //这里拿到的就是在Launcher构造函数中构造的AppClassLoader实例
        ......
        }
    }

2. tomcat7类加载器

tomcat作为一个java web容器,也有自己的类加载机制,通过自定义的类加载机制以实现共享类库的抽取,不同web应用之间的资源隔离还有热加载等功能。除了一些java自身的一些类加载器处,它实现的主要类加载器有:Common ClassLoader,Catalina ClassLoader,Shared ClassLoader以及WebApp ClassLoader.通过下面类关系图以及逻辑关系图,同时对比上文内容梳理这些类加载器之间的关系。

2.1 类关系图

classloader-3

从图中看到了Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader继承自WebAppClassLoaderBase,基本所有逻辑都在WebAppClassLoaderBase为中实现了,可以看出tomcat的所有类加载器都是以URLClassLoader为基础进行扩展。

2.2 逻辑关系图

classloader-4

上面说到Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,在默认的配置中,它们其实都是同一个对象,即commonLoader,结合初始化时的代码(只保留关键代码):

 private void initClassLoaders() {
        commonLoader = createClassLoader("common", null);  // commonLoader的加载路径为common.loader
        if( commonLoader == null ) {
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader); // 加载路径为server.loader,默认为空,父类加载器为commonLoader
        sharedLoader = createClassLoader("shared", commonLoader); // 加载路径为shared.loader,默认为空,父类加载器为commonLoader
    }
 private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;      // catalinaLoader与sharedLoader的加载路径均为空,所以直接返回commonLoader对象,默认3者为同一个对象
    }

在上面的代码初始化时很明确是指出了,catalina与shared类加载器的父类加载器为common类加载器,而初始化commonClassLoader时父类加载器设置为null,最终会调到createClassLoader静态方法:

 public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {
        .....
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);  //该构造方法默认获取系统类加载器为父类加载器,即AppClassLoader
                        else
                            return new URLClassLoader(array, parent);
                    }
                });

    }

createClassLoader中指定参数parent==null时,最终会以系统类加载器(AppClassLoader)作为父类加载器,这解释了为什么commonClassLoader的父类加载器是AppClassLoader.

一个web应用对应着一个StandardContext实例,每个web应用都拥有独立web应用类加载器(WebClassLoader),这个类加载器在StandardContext.startInternal()中被构造了出来:

 if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

这里getParentClassLoader()会获取父容器StandarHost.parentClassLoader对象属性,而这个对象属性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其实就是Catalina.parentClassLoader对象属性,再来跟踪一下Catalina.parentClassLoader,在Bootstrap.init()时通过反射调用了Catalina.setParentClassLoader(),将Bootstrap.sharedLoader属性设置为Catalina.parentClassLoader,所以WebClassLoader的父类加载器是Shared ClassLoader.

2.3 类加载逻辑

tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。具体的加载逻辑位于WebAppClassLoaderBase.loadClass()方法中,代码篇幅长,这里以文字描述加载一个类过程:

  1. 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
  2. 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  3. 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
  4. 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。

第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一样是违反了双亲委托。

3. 参考

《深入理解java虚拟机》
http://blog.csdn.net/zhoudaxia/article/details/35824249
http://blog.csdn.net/kkdelta/article/details/7758916

猜你喜欢

转载自blog.csdn.net/czmacd/article/details/54017027