MENU

Java 杂记(十)双亲委派模型

October 1, 2018 • Code

前言:Java 中的双亲委派模型是 Java 在进行类加载过程中的一个重要知识点。

类加载过程及类加载器

Java 文件在编译运行时,需要将 .java 文件编译成 .class 文件,同时将 .class 文件载入内存供 JVM 虚拟机运行,后一个过程我们称之为 类加载过程。这个过程主要分为三个大阶段:

  • 加载
  • 链接
  • 初始化

其中 加载 阶段最核心的任务就是将外部存储设备中的 .class 文件载入内存,生成 Class 对象。

而具体负责这个过程的类在 Java 中被称为类加载器。类加载器按照加载类的不同可分为三种:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)

其中 Bootstrap ClassLoader 由 C++ 实现,其他由 Java 实现,所以站在 JVM 的角度上考虑也可以分为两种。这些不同的类加载器的层次结构如下图:

图中越往上代表级别越高,为下一层的父加载器,所加载的类也越核心。

  • 由图可知,Bootstrap ClassLoader 是 Java 中最顶层的类加载器,被称为根加载器,负责将存放在 <JAVA_HOME>\lib 目录或 -Xbootclasspath 参数指定的路径中的类库加载到内存中。

  • Extension ClassLoader 负责加载 <JAVA_HOME>\lib\ext 目录或 java.ext.dirs 系统变量指定的路径中的所有类库。

  • Application ClassLoader 负责加载 classpath 下的文件,一般来说,如果没有指定或自定义类加载器,我们编写的代码默认会使用该类加载器

每当一个类需要被加载:

  • JVM 首先查看是否被与该类文件关联的加载器所加载过,有的话直接加载。
  • 如果没有,并不会直接调用关联加载器,而是去请求关联加载器的父加载器加载该类,父类采用相同的加载逻辑(缓存 - 父类 - 自身)。
  • 如果加载失败才调用自身的加载器并加入缓存。

该过程就是所谓的「双亲委派模型」。

每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就直接返回。

与「双亲委派模型」相关的重要方法

在 Java 中,除了最顶层的 Bootstrap ClassLoader 由 C++ 编写外,其他所有的类加载器都由 ClassLoader 类及其子类构成。下面研究一下 ClassLoader 类中几个重要的方法。

  • loadClass 方法:双亲委派机制的关键。从下面的代码可以看出「双亲委派模型」的实现逻辑就在该方法中。
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) {
                // 如果没有被加载过,则调用父加载器的 loadClass 方法执行相同逻辑
                    c = parent.loadClass(name, false);
                } else {
                // 如果父加载器为空则直接调用根加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
            // 如果还是没有加载成功,则调用自身的加载器的 findClass 方法进行加载操作
                // 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
                ...
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  • findClass:定义属于自身加载器的类加载操作,由子类实现完成。
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
  • defineClass 不可重写,内部调用 native 方法,作用是将得到的字节流在 JVM 内部转化为 Class 对象。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    // defineClass1 为 native 方法,将字节流数组转为 class 对象
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

由上面的分析可知,在自定义类加载器时

  • 必要条件:继承 ClassLoader类。

  • 如果单纯想改变类加载的加载方式及来源而不破坏双亲委派,只重写 findClass 方法即可。

  • 如果想破坏双亲委派,需重写 loadClass 方法。典型如 JDBC 等(上下文加载器),这样做的目的是为了提高灵活性。

使用 IDE 等工具时,在编写完需要自定义加载器加载的类后,需要移动其 class 文件并删除对应 java 文件,这样才能使父类加载器无法加载类从而调用我们自定义的类加载器。

注意,即便重写了 loadClass 方法,依旧不能使用自定义的类加载器加载自定义的 java.lang.String 类,因为自定义包不能以 java.xxx.xxx 开头,这是一种安全机制,如果以 java 开头,系统直接抛异常。

Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment