JavaJava 杂谈基础原理

阿里P7架构师通过源码浅析Java中的资源加载

2019-07-05  本文已影响4人  Java_苏先生

一. 前提

最近在做一个基础组件项目刚好需要用到JDK中的资源加载,这里说到的资源包括类文件和其他静态资源,刚好需要重新补充一下类加载器和资源加载的相关知识,整理成一篇文章。

二. 什么是类加载器

虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到了Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,而实现这个动作的代码模块称为”类加载器(ClassLoader)”。

类加载器虽然只用于实现类加载的功能,但是它在Java程序中起到的作用不局限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立类在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。上面这句话直观来说就是:比较两个类是否”相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这个两个类是来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必然”不相等”。这里说到的”相等”包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceOf关键字做对象所属关系判定等情况。

类和加载它的类加载器确定类在Java虚拟机中的唯一性这个特点为后来出现的热更新类、热部署等技术提供了基础。

三. 双亲委派模型

从Java虚拟机的角度来看,只有两种不同的类加载器

JDK中提供几个系统级别的类加载器

Java开发者开发出来的Java应用程序都是由上面四种类加载器相互配合进行类加载的,如果有必要还可以加入自定义的类加载器。其中,启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器之间存在着一定的关系:

上图展示的类加载器之间的层次关系称为双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的类加载器(Java中顶层的类加载器一般是Bootstrap ClassLoader),其他的类加载器都应当有自己的父类加载器。这些类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是通过组合(Composition)的关系实现。类加载器层次关系这一点可以通过下面的代码验证一下:

public class Main {
    public static void main(String[] args) throws Exception{
        ClassLoader classLoader = Main.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }
}
//输出结果,最后的null说明是Bootstrap ClassLoader
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4629104a
null

双亲委派模型的工作机制:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的类加载器中,只有当父类加载器反馈自己无法完成当前的类加载请求的时候(也就是在它的搜索范围中没有找到所需要的类),子类加载器才会尝试自己去加载类。不过这里有一点需要注意,每一个类加载器都会缓存已经加载过的类,也就是重复加载一个已经存在的类,那么就会从已经加载的缓存中加载,如果从当前类加载的缓存中判断类已经加载过,那么直接返回,否则会委派类加载请求到父类加载器。这个缓存机制在AppClassLoaderExtensionClassLoader中都存在,至于BootstrapClassLoader未知。

双亲委派模型的优势:使用双亲委派模型来组织类加载器之间的关系,一个比较显著的优点是Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang包中的类库,它存放在rt.jar中,无论使用哪一个类加载加载java.lang包中的类,最终都是委派给处于模型顶层的启动类加载器进行加载,因此java.lang包中的类如java.lang.Object类在应用程序中的各类加载器环境中加载的都是同一个类。试想,如果可以使用用户自定义的ClassLoader去加载java.lang.Object,那么用户应用程序中就会出现多个java.lang.Object类,Java类型体系中最基础的类型也有多个,类型体系的基础行为无法保证,应用程序也会趋于混乱。如果尝试编写rt.jar中已经存在的同类名的类通过自定义的类加载进行加载,将会接收到虚拟机抛出的异常。

双亲委派模型的实现:类加载器双亲委派模型的实现提现在ClassLoader的源码中,主要是ClassLoader#loadClass()中。

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
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 {
                //父加载器不为null,说明父加载器不是BootstrapClassLoader
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //父加载器为null,说明父加载器是BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            }
            catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            //所有的父加载加载失败,则使用当前的类加载器进行类加载
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }
}

四. 破坏双亲委派模型

双亲委派模型在Java发展历史上出现了三次比较大”被破坏”的情况:

四. JDK中提供的资源加载API

前边花大量的篇幅去分析类加载器的预热知识,是因为JDK中的资源加载依赖于类加载器(其实类文件本来就是资源文件的一种,类加载的过程也是资源加载的过程)。这里先列举出JDK中目前常用的资源(Resource)加载的API,先看ClassLoader中提供的方法。

1. ClassLoader提供的资源加载API

//1.实例方法
public URL getResource(String name)
//这个方法仅仅是调用getResource(String name)返回URL实例直接调用URL实例的openStream()方法
public InputStream getResourceAsStream(String name)
//这个方法是getResource(String name)方法的复数版本
public Enumeration<URL> getResources(String name) throws IOException
//2.静态方法
public static URL getSystemResource(String name)
//这个方法仅仅是调用getSystemResource(String name)返回URL实例直接调用URL实例的openStream()方法
public static InputStream getSystemResourceAsStream(String name)
//这个方法是getSystemResources(String name)方法的复数版本
public static Enumeration<URL> getSystemResources(String name)

总的来看,只有两个方法需要分析:getResource(String name)和getSystemResource(String name)。查看getResource(String name)的源码:

public URL getResource(String name) {
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = getBootstrapResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}

是否似曾相识?这里明显就是使用了类加载过程中类似的双亲委派模型进行资源加载,这个方法在API注释中描述通常用于加载数据资源如images、audio、text等等,资源名称需要使用路径分隔符’/‘。getResource(String name)方法中查找的根路径我们可以通过下面方法验证:

public class ResourceLoader {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ResourceLoader.class.getClassLoader();
        URL resource = classLoader.getResource("");
        System.out.println(resource);
    }
}
//输出:file:/D:/Projects/rxjava-seed/target/classes/

很明显输出的结果就是当前应用的ClassPath,总结来说:ClassLoader#getResource(String name)是基于用户应用程序的ClassPath搜索资源,资源名称必须使用路径分隔符’/‘去分隔目录,但是不能以’/‘作为资源名的起始,也就是不能这样使用:classLoader.getResource("/img/doge.jpg")。接着我们再看一下ClassLoader#getSystemResource(String name)的源码:

public static URL getSystemResource(String name) {
    //实际上Application ClassLoader一般不会为null
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        return getBootstrapResource(name);
    }
    return system.getResource(name);
}

此方法优先使用应用程序类加载器进行资源加载,如果应用程序类加载器为null(其实这种情况很少见),则使用启动类加载器进行资源加载。如果应用程序类加载器不为null的情况下,它实际上退化为ClassLoader#getResource(String name)方法。

总结一下:ClassLoader提供的资源加载的方法中的核心方法是ClassLoader#getResource(String name),它是基于用户应用程序的ClassPath搜索资源,遵循”资源加载的双亲委派模型”,资源名称必须使用路径分隔符’/‘去分隔目录,但是不能以’/‘作为资源名的起始字符,其他几个方法都是基于此方法进行衍生,添加复数操作等其他操作。getResource(String name)方法不会显示抛出异常,当资源搜索失败的时候,会返回null。

2. Class提供的资源加载API

java.lang.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);
}
public InputStream getResourceAsStream(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResourceAsStream(name);
    }
    return cl.getResourceAsStream(name);
}

从上面的源码来看,Class#getResource(String name)和Class#getResourceAsStream(String name)分别比ClassLoader#getResource(String name)和ClassLoader#getResourceAsStream(String name)只多了一步,就是搜索之前先进行资源名称的预处理resolveName(name),我们重点看这个方法做了什么:

private String resolveName(String name) {
    if (name == null) {
        return name;
    }
    if (!name.startsWith("/")) {
        Class<?> c = this;
        while (c.isArray()) {
            c = c.getComponentType();
        }
        String baseName = c.getName();
        int index = baseName.lastIndexOf('.');
        if (index != -1) {
            name = baseName.substring(0, index).replace('.', '/')
                                +"/"+name;
        }
    } else {
        name = name.substring(1);
    }
    return name;
}

逻辑相对比较简单

小结:如果看过我之前写过的一篇URL和URI相关的文章就清楚,实际上Class#getResource(String name)和Class#getResourceAsStream(String name)的资源名称处理类似于相对URL的处理,而”相对URL的处理”的根路径就是应用程序的ClassPath。如果资源名称以’/‘开头,那么相当于从ClassPath中加载资源,如果资源名称不以’/‘开头,那么相当于基于当前类的实际类型的包目录下加载资源。

实际上类似这样的资源加载方式在File类中也存在,这里就不再展开。

五. 小结

理解JDK中的资源加载方式有助于编写一些通用的基础组件,像Spring里面的ResourceLoader、ClassPathResource这里比较实用的工具也是基于JDK资源加载的方式编写出来。

说实话,类加载器的”双亲委派模型”和”破坏双亲委派模型”是常见的面试题相关内容,这里可以简单列举两个面试题:

希望这篇文章能帮助你理解和解决这两个问题。

写在最后

上一篇 下一篇

猜你喜欢

热点阅读