ClassLoader解惑

一、什么是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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容