JVM - 类加载器(一)

701 阅读6分钟

白蛇缘起0_1920X804.jpg

前言

之前我们已经了解过了 JVM的类加载机制,而提到类的加载就离不开类加载。本文将介绍 JVM 的 类加载 以及 双亲委派模型

JDK9 开始引入了模块化,类加载器和双亲委派模型有了一些变化,因此,我们将分两次讨论他们的区别。

本文基于 JDK8 以及之前的版本。

类加载器

从虚拟机规范的角度来说,JVM 分为 引导类加载器 Bootstrap ClassLoader非引导类加载器 两种类型的类加载器。不同的虚拟机的实现不尽相同,本文以最主流的 HotSpot 虚拟机为例。

类加载器的分类

主要包含以下四种类加载器,

  • Bootstrap ClassLoader:引导类加载器
    • 引导类加载器是由 C/C++ 语言编写的,无法作为对象被程序所引用
    • 主要用来加载 核心类库 (rt.jar) 中的类
    • 为安全考虑,启动类加载器只加载包名为 java、javax、sun 等开头的类
    • 加载 ExtClassLoader,AppClassLoader 并为它们指定父加载器
    • 引导类加载器没有父加载器
  • Extension ClassLoader:扩展类加载器
    • sun.misc.Launcher$ExtClassLoader 实现,父加载器是 启动类加载器
    • 负责加载 JAVA_HOME/lib/ext 目录中,或者被 java.ext.dirs 系统变量指定的路径中的所有类库
  • Application ClassLoader:系统类加载器
    • sun.misc.Launcher$AppClassLoader 实现,父加载器是 扩展类加载器
    • ClassLoader.getSystemClassLoader() 的返回值,因此也称为系统类加载器
    • 如果程序中没有自定义任何其他的类加载器,我们自己编写的类都是由系统类加载器进行加载的
  • User ClassLoader:自定义类加载器
    • 用户自定义的类加载器可以由用户实现并获取任意来源的二进制字节码进行加载

除了启动类加载器外,其他的类加载器都继承自 java.lang.ClassLoader,类加载具体逻辑封装在 loadClass() 方法中。

自定义类加载器

自定义一个类加载非常容易,只需要继承 java.lang.ClassLoader。在 jdk1.2 之前,继承 ClassLoader 时,要重写 loadClass() 方法,来实现自定义类的加载逻辑。在 jdk1.2 后,建议把自定义类加载逻辑写在 findClass() 方法中,而不是重写 loadClass()。

// 自定义类加载器
ClassLoader myClassLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
       // ...
    }
};

导致这种变化的原因是因为双亲委派机制的出现,我们后面会详细说明。

如果没有过于复杂的需求,可以直接继承 URLClassLoader 类实现自定义的类加载器,更加简单。

如何获取类加载器

如果我们要查看一个类是由哪个类加载器进行加载的,可以通过如下几种方法获取类加载。

  • 方式 1 :通过 class 对象的 getClassLoader() 方法
clazz.getClassLoader();
  • 方式 2 :线程获取上下文类加载器
Thread.currentThread().getContextClassLoader();
  • 方式 3:获取系统类加载器
ClassLoader.getSystemClassLoader();

类加载器的命名空间

JVM 规范中规定,每个类加载器都有一个属于自己的命名空间。即使我们使用不同的类加载器加载同一个类,也会导致这两个类不相等。

比如下面的例子中,我们自定义了一个类加载器 myClassLoader,并用它加载 ClassLoaderDemo02 这个类并获取它的一个实例 obj。然后我们用 instanceof 比较得到的结果却是 false

public class ClassLoaderDemo02 {
    public static void main(String[] args) throws Exception {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream resourceAsStream = getClass().getResourceAsStream(fileName);
                    if (resourceAsStream == null) {
                        return super.loadClass(name);
                    }
                    byte[] bytes = new byte[resourceAsStream.available()];
                    resourceAsStream.read(bytes);
                    return defineClass(name,bytes,0,bytes.length);
                }catch (IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };

        Object obj = myClassLoader.loadClass("com.zcat.jvm.classloader.ClassLoaderDemo02").newInstance();
        System.out.println(obj instanceof ClassLoaderDemo02); // 输出:false
    }
}

这是因为 ClassLoaderDemo02 本身是用 AppClassLoader 加载的,而 obj 是自定义类加载器加载的。

实际上,这种情况并不是我们所希望的,可能会导致某些错误。下面介绍的双亲委派模型就可以很好的解决这个问题。

双亲委派模型

双亲委派的原理

默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在使用时就不会产生歧义。

双亲委派机制 (Parents Delegation Model): 如果一个类加载器收到了类加载的请求,它不会自己先加载,而是将加载请求委托给父加载器加载;如果父加载器还有父加载器,继续向上委托,直到委托到最顶层的 启动类加载器

如果父加载器可以完成类加载器请求,就成功返回;如果父加载器无法完成请求,子类加载器才会尝试自己加载。

父加载器和子加载器之间不是继承关系,而是通过 组合 来实现的一种逻辑关系。

image.png

双亲委派的实现

双亲委派的实现写在了 loadClass 方法中,

// 以下代码并不完整,只保留了核心步骤代码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先检查该类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 没有被加载过,委托上级加载器加载
            try {
                // 如果 parent 不为空,调用 parent 的 loadClass
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // parent为空说明已经到了引导类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父类加载器抛出了 ClassNotFound异常,说明父加载器无法完成加载请求
            }
            if (c == null) {
                // 父加载器无法完成加载请求时,自己再加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

注意到代码中的 parent 是类的一个属性,这也印证了双亲委派是通过组合而不是继承实现的。

破坏双亲委派

双亲委派机制 并不是一个强制性的约束模型 ,这意味着这种机制可以被破坏。比如上面的 类加载器的命名空间 一节中,重写了 loadClass 方法其实就是对双亲委派的一次破坏,所以在 JDK1.2 引入双亲委派机制后,建议重写 findClass 而不是 loadClass,来避免破坏这种机制。

第一次破坏

上面已经提到,JDK1.2 之前,可以通过重写 loadClass() 方法,来破坏双亲委派模型。

第二次破坏

第二次破坏和 SPI(Service Provider Interface) 有关,典型的例子就是 JDBC。

各个厂商都需要实现自己的驱动,实现同一个接口 java.sql.Driver (该类由启动类加载器加载) ,但是启动类加载器不识别这些驱动。

为了解决这个问题,引入了 线程上下文加载器 ,该加载器可以通过 Thread.setContextClassLoader() 设置,如果线程还没有创建,会从父线程继承一个加载器;如果全局都没有设置过加载器,线程上下文加载器默认是 应用程序类加载器

这样就相当于用父类加载器来请求子类加载器去完成类加载,已经违背了双亲委派模型的自底向上的逻辑原则。

第三次破坏

第三次破坏与 程序的动态性 相关,比如热部署等。在这种情况下,树形结构的双亲委派显然无法满足这种复杂的需求,类加载变成了复杂的网状结构。

比如 JDK9 开始引入了 模块化 ,这也在一定程度上破坏了双亲委派机制。并且类加载的结构和实现方式都有了变化,一个类如果归属于某个模块,会优先委派给加载该模块的类加载器去加载,而不是直接委派给父加载器加载。

关于 JDK9 之后的类加载器和双亲委派,我们下次再具体讨论。