深入分析 ClassLoader

起因

针对下面这个代码

public static void loadFile(){
    try(InputStream is = XXX.class.getClassLoader.getResourceAsStream("xxxxx.txt"){
        //xxxxxx
    }
}

有同事提出为啥此处要用“XXX” claas,用其它的class是否可行?

然后我一通解释,甚至还般出了Thread.currentThread().getContextClassLoader().loadClass("xxx.xxx.xx")的方案。但是进一步探讨时,发现成功的给自己挖了个坑。有以下几个问题自己都解释不通。

  • Thread上下文获取到的类加载器与Class.getClassLoader()拿到的有什么区别
  • 在加载一个类的时候,是如何找到一个类的位置的
  • 在加载文件的文件,第三方jar包中的配置文件与自己项目下的Resource目录下的加载方式有没有什么区别,是否需要不同的类的ClassLoader
  • 在加载文件或者类资源的时候,对文件的命名有何要求

在上面几个问题的引导下,重新研究了下ClassLoader的相关文档,并且写例子验证了这些问题,从而有了此文。

ClassLoader原理阐述

ClassLoader,顾名思义,就是类加载器。当我们调用一个new语句时,相应的class文件需在被加载到Jvm中。此时ClassLoader就需要负责查找class,读class文件,校验,连接,最终加载到Jvm中

  • java提供的默认ClassLoader

    • BootStrap ClassLoader: 加载位于 System.getProperty("sun.boot.class.path") 处的class文件,因此可以通过sun.boot.class.path参数设置BootStrap ClassLoader负责加载的类
    • ExtClassLoader:加载位于System.getProperty("java.ext.dirs") 的类
    • AppClassLoader:也称SystemClassLoader, 加载位于System.getProperty("java.class.path")处的类
  • ClassLoader的双亲委托机制

    ClassLoader是一个虚类,除BootStrap ClassLoader外,每一个ClassLoader都需要继承ClassLoader,并且每个都有一个其父ClassLoader的引用(同样,BootStrap除外)。BootStrap ClassLoader是C++ 语言写的,在系统启动的时候,就会启动BootStrap类加载器。BootStrap ClassLoader是ExtClassLoader的父加载器,但是如果获取ExtClassLoader的父加载器,拿到是null;ExtClassLoader 是AppClassLoader的父加器
    但是双亲委托机制并不是绝对的,像后文中提到的SpringBoot的 RestartClassLoader就打破了该机制。
    在jvm启动的时候,会加载类sun.misc.Launcher,在该类的造函数中,会创建ExtClassLoader及AppClassLoader,并且把ExtClassLoader设置为AppClassLoader的父ClassLoader,同时设置线程的上下文加载器为AppClassLoader。

  • 类加载过程(默认的loadClass流程)

    • 首先AppClassLoader会检查是否加载过该类,如果加载过,直接返回
    • 没有加载过,委托父加载器进行加载(父加载器执行同样的流程)
    • 父加载器加载成功,直接返回,如果没有加载成功,则当前加载器开始查找Class再进行加载。

    ClassLoader进行了封装,对于子类,不建议覆盖loadClass流程,只实现其findClass方法即可,这样就可以不破坏加载器的双亲委机制。所以对于自定义的ClassLoader而言,实现如何查找类,在哪里查找类就是关键。

    对于查找资源文件,与加载类一致,在下面的源码分析中会提到。

    对于线程的上下文加载器,等下在源码分析中也可以看到一点,总体而言就是将一下加载器保存在线程的上下文中,在某些特定的场景中使用,比如tomcat中会使用到。同时线程的上下文加载器与当前的类可能并不一是致。

ClassLoader相关源码分析

com.sun.Launcher源码(分析默认的几个类加载器的构造过程)
public class Launcher {
    //系统的ClassLoader,后面会被赋值为AppClassLoader
    private ClassLoader loader;
    //BootStrap ClassLoader要加载的类的范围
    private static String bootClassPath = System.getProperty("sun.boot.class.path");

    public Launcher() {
        // 创建ExtClassLoader
        ClassLoader extcl = ExtClassLoader.getExtClassLoader();
        // 创建AppClassLoader,同时设置extcl为其父加载器
        loader = AppClassLoader.getAppClassLoader(extcl);
        //设置线程上下文类加载器为 AppClassLoader        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * 获取类加载器
     */
    public ClassLoader getClassLoader() {
        return loader;
    }

    /*
     * 具体加载ExtClassLoader的过程,并且可以看出,其继承自URLClassLoader
     */
    static class ExtClassLoader extends URLClassLoader {
        private File[] dirs;
        // 创建方法
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                return (ExtClassLoader) AccessController.doPrivileged(
                     new PrivilegedExceptionAction() {
                        public Object run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                    throw (IOException) e.getException();
            }
        }

        /*
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException {

            // 第二个参数就是parent ClassLoader,此处可以看到为null
            super(getExtURLs(dirs), null, factory);
            this.dirs = dirs;
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            // 处理s相关的内容
            return dirs;
        }
    }

    /**
     * 创建AppClassLoader 
     */
    static class AppClassLoader extends URLClassLoader {

        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            // 参数  System.getProperty("java.class.path")
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);
            return (AppClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        /*
         * Creates a new AppClassLoader
         */
        AppClassLoader(URL[] urls, ClassLoader parent) {

            // 此处可以看到ExtClassLoader 是 AppClassLoader的父ClassLoader
            super(urls, parent, factory);
        }
    }
}

上面代码可以看到,在创建Launcher的时候,创建了ExtClassLoader,并且将其parent ClassLoader设置为null,创建了AppClassLoader将ExtClassLoader设置为其parent ClassLoader。 同时可以看到每个ClassLoader都继承自URLClassLoader,并且可以看到各ClassLoader负责加载的类的范围

分析ClassLoader的源码
public abstract class ClassLoader {
    // 保存的地父加载器的引用
    private final ClassLoader parent;

    // 加载class文件的核心代码
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果父加载器不为空,则委托父加载器进行加载,可以看到双亲委托机制
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 加载器为空,即ExtClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                //没有加载到类,抛异常后,吞掉
                }

                // 如果父加载器没有加载到,轮到自己进行加载
                if (c == null) {
                    // 这个方法是一个虚方法,还未实现,子类实现即可
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }    
}

从上面代码可以看到,查找类的时候,采用双亲委托机制,先委托父类进行查找类,如果找不到自己再进行查找。

再来简单介绍一个URLClassLoader:ClassLoader中有一个URLClassPath属性,URLClassPath包含一个List的属性,该属性就保存着当前ClassLoader可以查找的范围。当URL以’/’结尾时,说明是一个directory,当是以非’/’结尾的时候,说明是一个jar包。当查找类或者资源文件时,遍历该URL集合,根据是directory还是jar包分别进行处理。

再看查找资源文件,getResource的方法:

   //class 的getResource文件, Class 类
   public java.net.URL getResource(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        if (cl==null) {
            // A system class.
            return ClassLoader.getSystemResource(name);
        }
        return cl.getResource(name);
    }

可以看到,Class.getResource() 最后是通过调用ClassLoader.getResource()实现的。当前在此之前,先进行了resolveName(name)方法。该方法的作用是获取该资源文件相关对于ClassPath的路径,即如果以’/’打头,则认为是一个完整路径,去掉’/’,如果以非’/’打头,则认为是相对当前类文件的地方,则会将当前Class文件的package名加到当明的name前,并且将’.’用’/’替换,形成相对于ClassPath的路径。

ClassLoader的的getResouce的方法

// 同样执行双亲委托机制
public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            // findResource 是一个虚方法,子类可以实现,与类加载的过程类似。
            // 而对于AppClassLoader以及ExtClassLoader,即从adsw包含的Url属性中查找相关的资源,包含从jar文件中及目录中。
            url = findResource(name);
        }
        return url;
    }

实验证明

  • 自定义类加载器,加载非classPath下的文件,并且验证不同的类加载器加载到的类是否是同一个类

    //先定义一个我们想要加载的类
    package com.person;
    
    public class Test{
      // 该方法输出自己的类加载器
      public void sayClassLoader(){
        System.out.println("classLoader:" + Test.class.getClassLoader());
      }
    }
    
    public class MainTest {
    
      public static void main(String[] args) throws Exception {
        // 我们把刚定义的java文件编译,放在/tmp/com/person下,其中 ‘/com/person’是对应java文件的package
        String path = "/tmp/";
        URL url = new File(path).toURI().toURL();
        List<Class<?>> classes = Lists.newArrayList();
        for (int i = 0; i < 2; i++) {
          Thread thread = new Thread(() -> {
            try {
              //使用自定义的ClassLoader
              MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader());
              Class<?> aClass = loader.loadClass("com.person.Test");
              // 反射构实例以及调用 sayClassLoader 方法
              Object instance = aClass.newInstance();
              Method method = aClass.getMethod("sayClassLoader");
              method.invoke(instance);
              classes.add(aClass);
            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
              e.printStackTrace();
            }
          });
          thread.start();
        }
        // 等待两个线程执行完毕
        Thread.sleep(2000);
        //判定两个类是否是同一个类
        System.out.println(classes.get(0) == classes.get(1));
      }
    }
    
    //自定义的类加载器
    class MyClassLoader extends URLClassLoader{
    
      public MyClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
      }
    }

    执行结果

    classLoader:com.yqg.collection.MyClassLoader@20c489d2
    classLoader:com.yqg.collection.MyClassLoader@5e9b8a25
    false

    上面案例中,自定义的类加载器继承了URLClassLoader,通过上面的case可以看出,自定义的类加载器加载了自定义的目录上的文件,并且发现两个ClassLoader加载的类,即使是同一个类文件,但是也不是同一个类。

  • 验证类的双新委托机制

    public static void main(String[] args) throws Exception {
    URLClassLoader classLoader = (URLClassLoader) MainTest.class.getClassLoader();
    // 这个反射的方法用于增加该ClassLoader的查找范围,现在增加了/tmp目录
    Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    method.setAccessible(true);
    String path = "/tmp/";
    URL url = new File(path).toURI().toURL();
    method.invoke(classLoader, url);
    
    //用自定义的类加载器进行加载
    MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader());
    Class<?> aClass = loader.loadClass("com.person.Test");
    
    // 输出最终类的加载器
    System.out.println(aClass.getClassLoader());
    }

    运行结果如下

    sun.misc.Launcher$AppClassLoader@18b4aac2

    从上面看出,com.person.Test类虽然是通过调用MyClassLoader进行加载但是根据双亲委托机制,最终由其父加载器AppClassLoader完成加载。

结论

  • 类通过ClassLoader进行加载,ClassLoader持有其父加载器的引有,常规情况下,当需要加载一个类或者一个资源文件时,委托父加载器进行加载,当父加载器加载不到时,由当前加载器进行加载。但是这并不是必须的,像ogsi,tomcat就都有违反此机制的情况。
  • 加载资源文件时,与加载类文件一致,只要加载器是一样的,其可加载的范围就是一样的,配置文件可以是当前工程的配置文件,也可能是第三方jar的配置文件。
  • 当有多个资源文件时,与类加载器的搜索顺序相关。所以可以在findResouce的时候,设定优先级及顺序。
  • 不同的类加载器加载到的类文件,不是同一个类,这个可用于在一些特定的场景中,比如tomcat的WebappClassLoader用于隔离多个Context,SpringBoot的RestartClassLoader仅用于加载适用于热更新的类,像第三方jar包则由其它的类加载器进行加载。
  • 查找类文件与查找检查资源文件基本上是一致的,也取决于类加载器的实现。

其它case分析(SpringBoot RestartClassLoader)

// 同样继承了URLClassLoader
// 该类加载器只关心可热更新的类(自己项目),而第三方jar包会有其它的ClassLoader进行加载,当项目中的类发生变更时,只需替换当前项目中的类即可,不需要加载全部的类
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {

    /**
     * Create a new {@link RestartClassLoader} instance.
     * @param parent the parent classloader
     * @param updatedFiles any files that have been updated since the JARs referenced in
     * URLs were created.
     * @param urls the urls managed by the classloader
     * @param logger the logger used for messages
     */
    public RestartClassLoader(ClassLoader parent, URL[] urls,
            ClassLoaderFileRepository updatedFiles, Log logger) {
        super(urls, parent);
        Assert.notNull(parent, "Parent must not be null");
        Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
        Assert.notNull(logger, "Logger must not be null");
        this.updatedFiles = updatedFiles;
        this.logger = logger;
        if (logger.isDebugEnabled()) {
            logger.debug("Created RestartClassLoader " + toString());
        }
    }

    // 重写loadClass方法
    public Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        String path = name.replace('.', '/').concat(".class");
        // 先在当前的updatedFiles中找
        ClassLoaderFile file = this.updatedFiles.getFile(path);
        if (file != null && file.getKind() == Kind.DELETED) {
            throw new ClassNotFoundException(name);
        }
        Class<?> loadedClass = findLoadedClass(name);
        //  如是没有找到
        if (loadedClass == null) {
            try {
                // 先自行进行加载
                loadedClass = findClass(name);
            }
            catch (ClassNotFoundException ex) {
                //如果没有加载到,再调用父加载器进行加载,从这个地方可以看到,为了满足热更新,违背了双亲委托原则。
                loadedClass = getParent().loadClass(name);
            }
        }
        if (resolve) {
            resolveClass(loadedClass);
        }
        return loadedClass;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值