ClassLoader解惑

作者: 阿里加多 | 来源:发表于2017-06-06 09:09 被阅读375次

    一、什么是Classloader

    一个Java程序要想运行起来,首先需要经过编译生成 .class文件,然后创建一个运行环境(jvm)来加载字节码文件到内存运行,而.class 文件是怎样被加载中jvm 中的就是Java Classloader所做的事情。

    那么.class文件什么时候会被类加载器加载到jvm中运行那?比如执行new操作时候,当我们使用Class.forName("包路径+类名"),Class.forName("包路径+类名",classloader),classloader.loadclass("包路径+类名");时候就触发了类加载器去类加载对应的路径去查找*.class,并创建Class对象。另外理解 cl只能加载jar里面的文件夹里面的class文件,不能加载jar里面嵌套的jar里面的class,这个很重要

    阿里巴巴长期招聘Java研发工程师p6,p7,p8等上不封顶级别,有意向的可以发简历给我,注明想去的部门和工作地点:1064454834@qq.com

    二、Java自带的Classloader

    2.1 BootstrapClassloader

    引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载Java核心类,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值可以指定附加的类,它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的该类c 语言实现,Java程序访问不到该加载器。通过下面代码可以查看该加载器加载了哪些jar包

    public void test() {  
            URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();    
            for (int i = 0; i < urls.length; i++) {    
                System.out.println(urls[i].toExternalForm());    
            }   
        }  
    

    执行结果:
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/resources.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/sunrsasign.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jsse.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jce.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/charsets.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jfr.jar
    file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/classes,
    写到这里大家应该都知道,我们并没有在classpath里面指定这些类的路径,为啥还是能被加载到jvm并使用起来了吧,因为这些是bootstarp来加载的。

    2.2 ExtClassloader

    扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系统属性指定的jar包。放入这个目录下的jar包对所有AppClassloader都是可见的(后面会知道ExtClassloader是AppClassloader的父加载器)。那么ext都是在那些地方加载类内:

    System.out.println(System.getProperty("java.ext.dirs"));  
    

    /Users/zhuizhumengxiang/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

    2.3 AppClassloader

    系统类加载器,又称应用加载器,本文说的SystemClassloader和APPClassloader是一个东西,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。调用ClassLoader.getSystemClassLoader()可以获取该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过ClassLoader的无参构造函数可以知道如下:

     protected ClassLoader() {
            this(checkCreateClassLoader(), getSystemClassLoader());
        }
    

    执行以下代码即可获得classpath加载路径:

    System.out.println(System.getProperty("java.class.path"));
    

    2.4 三种加载器联系

    用一张图来表示三张图的关系如下:


    screenshot.png

    用户自定义的无参加载器的父类加载器默认是AppClassloader加载器,而AppClassloader加载器的父加载器是ExtClassloader,通过下面代码可以验证:

    ClassLoader.getSystemClassLoader().getParent()
    

    一般我们都认为ExtClassloader的父类加载器是BootStarpClassloader,但是其实他们之间根本是没有父子关系的,只是在ExtClassloader找不到要加载类时候会去委托BootStrap加载器去加载。
    通过如下代码可以知道父加载器为null

    ClassLoader.getSystemClassLoader().getParent().getParent()
    

    2.5 类加载器原理

    Java类加载器使用的是委托机制,也就是子类加载器在加载一个类时候会让父类来加载,那么问题来了,为啥使用这种方式那?因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。下面我们从源码看如何实现委托机制:

    protected Class<?> loadClass(Stringname,boolean resolve)  
           throws ClassNotFoundException  
       {  
           synchronized (getClassLoadingLock(name)) {  
               // 首先从jvm缓存查找该类
               Class c = findLoadedClass(name);  (1)
               if (c ==null) {  
                   longt0 = System.nanoTime();  
                   try {  //然后委托给父类加载器进行加载
                       if (parent !=null) {  
                           c = parent.loadClass(name,false);  (2)
                       } else {  //如果父类加载器为null,则委托给BootStrap加载器加载
                           c = findBootstrapClassOrNull(name);  (3)
                       }  
                   } catch (ClassNotFoundExceptione) {  
                       // ClassNotFoundException thrown if class not found  
                       // from the non-null parent class loader  
                   }  
      
                   if (c ==null) {  
                       // 若仍然没有找到则调用findclass查找
                       // to find the class.  
                       longt1 = System.nanoTime();  
                       c = findClass(name);  (4)
      
                       // 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);  //链接,文件格式,字节码验证,符号引用转为直接引用等
               }  
               returnc;  
           }  
       }  
    

    分析代码知道首先会执行(1)从jvm缓存查找该类,如何该类之前被加载过,则直接从jvm缓存返回该类,否者看当前类加载器是否有父加载器,如果有的话则委托为父类加载器进行加载(2),否者调用(3)委托为BootStrapClassloader进行加载,如果还是没有找到,则调用当前Classloader的findclass方法进行查找。
    从上面源码知道要想修改类加载委托机制,实现自己的载入策略 可以通过覆盖ClassLoader的findClass方法或者覆盖loadClass方法来实现。

    2.6 Java中如何构造三种类加载器的结构

    下面从源码来分析下JVM是如何构建内置classloader的,具体是rt.jar包里面sun.misc.Launcher类:

    public Launcher()  
          {  
            ExtClassLoader localExtClassLoader;  
            try  
            {  //首先创建了ExtClassLoader
              localExtClassLoader = ExtClassLoader.getExtClassLoader();  
            }  
            catch (IOException localIOException1)  
            {  
              throw new InternalError("Could not create extension class loader");  
            }  
            try  
            {  //然后以ExtClassloader作为父加载器创建了AppClassLoader
              this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);  
            }  
            catch (IOException localIOException2)  
            {  
              throw new InternalError("Could not create application class loader");  
            }  //这个是个特殊的加载器后面会讲到,这里只需要知道默认下线程上下文加载器为appclassloader
            Thread.currentThread().setContextClassLoader(this.loader);  
              
            ................
          }  
    

    下面看下ExtClassLoader.getExtClassLoader()的代码

    public static ExtClassLoader getExtClassLoader()  
          throws IOException  
        {  //可以知道ExtClassLoader类加载路径为java.ext.dirs
          File[] arrayOfFile = getExtDirs();  
          try  
          {  
            (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction()  
            {  
              public Launcher.ExtClassLoader run()  
                throws IOException  
              {  
                int i = this.val$dirs.length;  
                for (int j = 0; j < i; j++) {  
                  MetaIndex.registerDirectory(this.val$dirs[j]);  
                }  
                return new Launcher.ExtClassLoader(this.val$dirs);  
              }  
            });  
          }  
          catch (PrivilegedActionException localPrivilegedActionException)  
          {  
            throw ((IOException)localPrivilegedActionException.getException());  
          }  
        }  
          
      
        private static File[] getExtDirs()  
        {  
          String str = System.getProperty("java.ext.dirs");  
          File[] arrayOfFile;  
          if (str != null)  
          {  
            StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);  
              
            int i = localStringTokenizer.countTokens();  
            arrayOfFile = new File[i];  
            for (int j = 0; j < i; j++) {  
              arrayOfFile[j] = new File(localStringTokenizer.nextToken());  
            }  
          }  
          else  
          {  
            arrayOfFile = new File[0];  
          }  
          return arrayOfFile;  
        }  
    

    下面看下AppClassLoader.getAppClassLoader的代码

    public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)  
          throws IOException  
        {  //可知AppClassLoader类加载路径为java.class.path
          String str = System.getProperty("java.class.path");  
          final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);  
            
          (ClassLoader)AccessController.doPrivileged(new PrivilegedAction()  
          {  
            public Launcher.AppClassLoader run()  
            {  
              URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);  
                
              return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);  
            }  
          });  
        }  
    

    总结下Java应用启动过程是首先BootstarpClassloader加载rt.jar包里面的sun.misc.Launcher类,而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,然后在根据不同场景去使用这些类加载器去自己的类查找路径去加载类。

    三、一种特殊的类加载器-ContextClassLoader

    阅读过tomcat和集团中间件源码的童鞋对ContextClassLoader应该很熟悉了,可以很多地方看到下面的结构的用法:

    //获取当前线程上下文类加载器
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    try {//设置当前线程上下文类加载器为targetTccl
        Thread.currentThread().setContextClassLoader(targetTccl);
        //doSomething 
        doSomething();
    } finally {//设置当前线程上下文加载器为原始加载器
        Thread.currentThread().setContextClassLoader(classLoader);
    }
    

    doSomething里面则调用了 Thread.currentThread().getContextClassLoader()获取了当前线程上下文类加载器来做了某些事情。那么这其中的奥秘和使用场景是哪里那?我们知道Java默认的类加载机制是委托机制,但是有时这种加载顺序不能正常工作,通常发生在有些JVM核心代码必须动态加载由应用程序开发人员提供的资源时。以JNDI举例:它的核心内容和接口在rt.jar中的引导类中实现了,但是这些JNDI实现类可能加载由独立厂商实现和部署在应用程序的classpath中的JNDI提供者。这个场景要求一个父类加载器(这个例子中指加载rt.jar的bootstarp加载器)去加载一个在它的子类加载器(AppClassLoader)中可见的类。此时通常的J2SE委托机制就不能胜任,解决办法是让JNDI核心类使用线程上下文加载器(从2.6节知道默认线程上下文加载器为AppClassLoader)。

    具体来说在比如Java中的spi,SPI的全名为Service Provider Interface,spi是面向接口编程,服务规则提供者会在Java核心类理他提供服务访问接口,而具体实现则有其他开发商提供,我们知道Java核心类,例如rt.jar包是有bootstrap加载,而用户提供的jar包在由appclassloader加载。另外我们知道如果一个类是有A加载器加载,那么A类依赖或者引用的类也是有相同的加载器加载。那么有bootstarp加载的类怎么加载到本来应该有appclassloadr加载的类那,这时候线程上下文类加载就派上用处了。

    在 例如jdbc4也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.JDBC服务提供商只需要实现java.sql.dirver就可以了。例如mysql驱动


    screenshot.png

    而mysql的驱动如下实现了java.sql.Driver

    public class com.mysql.jdbc.Driver extends com.mysql.jdbc.NonRegisteringDriver implements java.sql.Driver
    

    我们写个测试代码,看看是如何工作的:

    public class MyContextClassLoad {
    
         public static void testContextClassLoader() {
                ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
                Iterator<Driver> iterator = loader.iterator();
                while (iterator.hasNext()) {
                    Driver driver = (Driver) iterator.next();
                    System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
                }
            System.out.println("current thread contextloader:"+Thread.currentThread().getContextClassLoader());
    
                System.out.println("current loader:" + MyContextClassLoad.class.getClassLoader());
            System.out.println("ServiceLoader loader:" + ServiceLoader.class.getClassLoader());
            }
        
        
        public static void main(String []arg){
            
            testContextClassLoader();
        }
    }
    

    执行结果如下:
    driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@53d9f80

    current thread context loader:sun.misc.Launcher$AppClassLoader@53d9f80

    current loader:sun.misc.Launcher$AppClassLoader@53d9f80
    ServiceLoader loader:null
    从执行结果可以知道ServiceLoader的加载器为Bootstarp,因为这里输出了null,并且从该类在rt.jar里面也证明了这个说法.而com.mysql.jdbc.Driver则使用AppClassLoader加载。我们知道如果一个类中引用了另外一个类,那么这被引用的类也应该由引用方类加载器来加载,而现在则是引用方ServiceLoader使用BootStarpClassloader加载,被引用方则使用伪子加载器APPClassLoader来加载了,是不是很诡异

    下面我们来看下源码:

    public final class ServiceLoader<S> implements Iterable<S> {
        public static <S> ServiceLoader<S> load(Class<S> service) {
            // 获取当前线程上下文,本例子里面是AppClassLoader
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    
        public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
            return new ServiceLoader<>(service, loader);
        }
    
        private ServiceLoader(Class<S> svc, ClassLoader cl) {
            service = svc;
            loader = cl;
            reload();
        }
    
        public S next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                // 这里使用AppClassLoader加载mysql实现的spi类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service, "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service, "Provider " + cn + " could not be instantiated", x);
            }
            throw new Error(); // This cannot happen
        }
    }
    

    我们在做另外一个实验:

    package myclassloader;
    
    import java.io.FileNotFoundException;
    import java.net.URL;
    import org.springframework.transaction.annotation.Transactional;
    
    public static void testContextClassLoader() {
            //获取extclassloader
        ClassLoader extClassloader = MyContextClassLoad.class.getClassLoader().getParent();
       System.out.println("extloader:"  +extClassloader);
           //设置当前线程上下文加载器为ext
         Thread.currentThread().setContextClassLoader(extClassloader);
           ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
           Iterator<Driver> iterator = loader.iterator();
           while (iterator.hasNext()) {
               Driver driver = (Driver) iterator.next();
               System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
           }
           System.out.println("current thread context loader:"  +Thread.currentThread().getContextClassLoader());
           System.out.println("current loader:" + MyContextClassLoad.class.getClassLoader());
           System.out.println("ServiceLoader loader:" + ServiceLoader.class.getClassLoader());
       }
    

    extloader:sun.misc.Launcher$ExtClassLoader@10b28f30

    current thread context loader:sun.misc.Launcher$ExtClassLoader@10b28f30

    current loader:sun.misc.Launcher$AppClassLoader@53d9f80

    ServiceLoader loader:null

    从结果可以知道没有加载到mysql驱动类,这是因为mysql的jar包是放到了classpath下,而extclassloader查找路径为ext目录所致。

    总结下:当父类加载器需要加载子类加载器中的资源时候可以通过设置和获取线程上下文类加载器来实现,其实另外还有一种情况就是一个类加载器要使用不在当前类加载器类查找路径路径中的情况,这种情况下可以新建一个在指定路径查找类的类加载器,其实下面第五节要讲的pandora类加载机制就是这样。

    四、Tomcat ClassLoader

    4.1 Tomcat classloader的构造

    首先我们打开tomcat的源码Bootstrap类的initClassLoaders方法:

        private void initClassLoaders() {
            try {
                // 创建commonLoader,父类为APPClassLoader
                commonLoader = createClassLoader("common", this.getClass().getClassLoader());
    
                if( commonLoader == null ) {
                    commonLoader=this.getClass().getClassLoader();
                }
               //以commonLoader为父加载器创建catalinaLoader和sharedLoader加载器
                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 {
            //获取catalina.properties中配置,分别为:common.loader,server.loader,shared.loader
            String value = CatalinaProperties.getProperty(name + ".loader");
           
           //根据配置知道server.loader,shared.loader为空,所以commonLoader=serverLoader=sharedLoader
            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;
                }
    
                // Check for a JAR URL repository
                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));
                }
            }
           //这里其实是URLClassLoader,只是urls会不同
            return ClassLoaderFactory.createClassLoader(repositories, parent);
        }
    

    这里根据配置文件catalina.properties里面配置的仓库地址作为类加载器查找类路径,配置如下:
    common.loader=${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar
    server.loader=
    shared.loader=
    根据上面代码可知commonLoader,serverLoader,sharedLoader是同一个classloader。

    然后继续看bootstarp的init方法:

        public void init()
            throws Exception
        {
            .....
            //上面分析的
            initClassLoaders();
    
            //设置当前线程上下文加载器为catalinaLoader
            Thread.currentThread().setContextClassLoader(catalinaLoader);
            SecurityClassLoad.securityClassLoad(catalinaLoader);
            
            //使用catalinaLoader加载Catalina类
            Class<?> startupClass =
                catalinaLoader.loadClass
                ("org.apache.catalina.startup.Catalina");
            Object startupInstance = startupClass.newInstance();
    
            // 设置Catalina的父加载器为sharedLoader.
            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;
        }
    

    这里设置了Catalina里面的parentClassLoader=sharedLoader,下面我们看web应用的classloader如何被创建。

    研究过tomcat的童鞋应该都知道tomcat的容器构造:


    screenshot.png

    其中Engine是最大的容器默认为StandardEngine,其中可以有若干个host默认为StandardHost,host的父容器为Engine,每个host容器里面有若干context容器默认为StandardContext,context容器的父容器为host.

    从catalina.java的createStartDigester函数:

      digester.addRule("Server/Service/Engine",
                             new SetParentClassLoaderRule(parentClassLoader));
    

    知道StandardEngine中的parentClassLoader被设置为了sharedLoader。

    然后我们看下StandardContext中的startInternal方法:

     if (getLoader() == null) {
                WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
                webappLoader.setDelegate(getDelegate());
               //设置到类对象
                setLoader(webappLoader);
            }
    

    其中getParentClassLoader方法用来获取父类加载器这里为sharedLoader,具体原因看如下代码:

        public ClassLoader getParentClassLoader() {
            if (parentClassLoader != null)
                return (parentClassLoader);
            if (getPrivileged()) {
                return this.getClass().getClassLoader();
            } else if (parent != null) {
                //这里StandardContext的parentClassLoader为空,则会调用StandardHost的parentClassLoader方法,也为空,则会调用StandardEngine的parentClassLoader方法,而它返回的正是sharedLoader。
                return (parent.getParentClassLoader());
            }
            return (ClassLoader.getSystemClassLoader());
        }
    

    上面创建了WebappLoader并且设置到了StandardContext的loader属性,下面调用loader的start方法启动web加载器的创建:

    if ((loader != null) && (loader instanceof Lifecycle))
                        ((Lifecycle) loader).start();
    

    进入WebappLoader的startInternal方法:

     protected void startInternal() throws LifecycleException {
            ..........
            // Construct a class loader based on our current repositories list
            try {
                if (classLoader == null) {//这里创建web应用类加载器
                    classLoader = createClassLoader();
                }
                classLoader.setResources(container.getResources());
                classLoader.setDelegate(this.delegate);
                classLoader.setSearchExternalFirst(searchExternalFirst);
                if (container instanceof StandardContext) {
                    classLoader.setAntiJARLocking(
                            ((StandardContext) container).getAntiJARLocking());
                    classLoader.setClearReferencesRmiTargets(
                            ((StandardContext) container).getClearReferencesRmiTargets());
                    classLoader.setClearReferencesStatic(
                            ((StandardContext) container).getClearReferencesStatic());
                    classLoader.setClearReferencesStopThreads(
                            ((StandardContext) container).getClearReferencesStopThreads());
                    classLoader.setClearReferencesStopTimerThreads(
                            ((StandardContext) container).getClearReferencesStopTimerThreads());
                    classLoader.setClearReferencesHttpClientKeepAliveThread(
                            ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
                }
    
                for (int i = 0; i < repositories.length; i++) {
                    classLoader.addRepository(repositories[i]);
                }
    
            } catch (Throwable t) {
            .......
            }
    
            setState(LifecycleState.STARTING);
        }
    

    createClassLoader的代码如下:

     /**
         * Create associated classLoader.
         */
    private String loaderClass =
            "org.apache.catalina.loader.WebappClassLoader";
        private WebappClassLoaderBase createClassLoader()
            throws Exception {
           //创建WebappClassLoader类
            Class<?> clazz = Class.forName(loaderClass);
            WebappClassLoaderBase classLoader = null;
    
            if (parentClassLoader == null) {
                parentClassLoader = container.getParentClassLoader();
            }
           //设置WebappClassLoader的父加载器为sharedLoader
            Class<?>[] argTypes = { ClassLoader.class };
            Object[] args = { parentClassLoader };
            Constructor<?> constr = clazz.getConstructor(argTypes);
            classLoader = (WebappClassLoaderBase) constr.newInstance(args);
    
            return classLoader;
    
        }
    

    至此创建了应用的类加载器,由于每个standardcontext对应一个web应用,所以不同的应用都有不同的
    WebappClassLoader,共同点是他们的父加载器都是sharedLoader。下面列下tomcat类加载器关系图:


    screenshot.png

    4.2 Tomcat classloader

    tomcat原生加载器:


    screenshot.png

    原生tomcat加载器里面查找,下面说说tomcat自身的web类加载器逻辑:

     public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
            synchronized (getClassLoadingLockInternal(name)) {
                ........
                Class<?> clazz = null;
        
                ........
                // (0) 首先检查webloader缓存中是否已经加载该类
                clazz = findLoadedClass0(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Returning class from cache");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
        
                // (0.1) 看jvm缓存是否已经加载该类
                clazz = findLoadedClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Returning class from cache");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
        
                // (0.2) 尝试在extClassloader查找该类
                try {
                    clazz = j2seClassLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
        
               .....
        
                boolean delegateLoad = delegate || filter(name);
        
                // (1) 如果在context.xml配置了代理,则委托给父类加载器sharedclassloader来加载
                if (delegateLoad) {
                    if (log.isDebugEnabled())
                        log.debug("  Delegating to parent classloader1 " + parent);
                    try {
                        clazz = Class.forName(name, false, parent);
                        if (clazz != null) {
                            if (log.isDebugEnabled())
                                log.debug("  Loading class from parent");
                            if (resolve)
                                resolveClass(clazz);
                            return (clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        // Ignore
                    }
                }
        
                // (2) 在web应用的WEB-INF/lib下查找
            
                try {
                    clazz = findClass(name);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from local repository");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
        
                // (3) 如果没有context.xml配置代理,则委托给父类加载器sharedclassloader来加载,和(1)只有一个存在
                if (!delegateLoad) {
                    if (log.isDebugEnabled())
                        log.debug("  Delegating to parent classloader at end: " + parent);
                    try {
                        clazz = Class.forName(name, false, parent);
                        if (clazz != null) {
                            if (log.isDebugEnabled())
                                log.debug("  Loading class from parent");
                            if (resolve)
                                resolveClass(clazz);
                            return (clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        // Ignore
                    }
                }
            }
            
            throw new ClassNotFoundException(name);
        }
    

    其中(0.2) 之前一直以为是appclassloader来加载,但是源码后发现竟然是extclassloader.

    public WebappClassLoaderBase(ClassLoader parent) {
    
            super(new URL[0], parent);
    
            ClassLoader p = getParent();
            if (p == null) {
                p = getSystemClassLoader();
            }
            this.parent = p;
            
            ClassLoader j = String.class.getClassLoader();
            if (j == null) {
                j = getSystemClassLoader();
                while (j.getParent() != null) {
                    j = j.getParent();
                }
            }
            this.j2seClassLoader = j;
    
            securityManager = System.getSecurityManager();
            if (securityManager != null) {
                refreshPolicy();
            }
        }
    

    其实在tomcat里面使用ContextClassloader的地方也随处可以,还比如StandardContext中的startInternal方法,我们知道StandardContext是有catalinaclassloader加载的,而startInternal里面则可以创建我们在web.xml中配置的listener和filter(这些明显应该由webappclassloader加载)

    //绑定当前线程上下文加载器为webappclassloader
    oldCCL = bindThread();
    
    try {
       
        // Configure and call application event listeners
        if (ok) {
            if (!listenerStart()) {
             
                ok = false;
            }
        }
        
       
        // Configure and call application filters
        if (ok) {
            if (!filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
        }
        
        // Load and initialize all "load on startup" servlets
        if (ok) {
            if (!loadOnStartup(findChildren())){
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }
        }
        
        // Start ContainerBackgroundProcessor thread
        super.threadStart();
    } finally {
        // Unbinding thread
        unbindThread(oldCCL);
    }
    

    总结下,默认情况下tomcat中commonloader,sharedloader,catalinaloader是同一个加载器,其类查找路径都是同一个地方。其实catalinaloader主要工作应该是加载tomcat本身启动所需要的类,而sharedloader是webappclassloader的父类,所以应该是加载一些所有webap共享的类,而commonlaoder作为sharedloader,catalinaloader的父类,自然设计目的是为了加载二者共享的类。所以如果能恰当的使用tomcat中设计的这种策略,修改catalina.properites中三种加载器类加载路径,就会真正达到这种设计效果。

    欢迎关注微信公众号:‘技术原始积累’ 获取更多技术干货__

    image.png

    相关文章

      网友评论

      本文标题:ClassLoader解惑

      本文链接:https://www.haomeiwen.com/subject/yjhgfxtx.html