Skip to content
Go back

Java 类加载机制详解

Published:  at  02:00 PM

这次来了解一下 Java 的类加载机制。

Table of contents

Open Table of contents

内置类加载器

首先介绍 JDK 内置的类加载器:

BootstrapClassLoader 也就是根加载器,该加载器是最顶级的加载器,没有任何父类加载器,由 C++ 编写,负责虚拟机核心类库的加载,如 String 等。

System.out.println(String.class.getClassLoader());
// 根加载器是获取不到引用的,所以输出为
// null

ExtClassLoader 即扩展类加载器,用于加载 JAVA_HOME 下的 jre/lib/ext 等目录下的类库。

ClassLoader cl = JarFileSystemProvider.class.getClassLoader();
System.out.println(cl);
System.out.println(cl.getParent());
// 该类是在 jre/lib/ext 下面的 jar 包中,所以输出为
// sun.misc.Launcher$ExtClassLoader@3cd1a2f1
// null

AppClassLoader 也就是应用加载器或者系统类加载器,负责加载 classpath 下面的 class 文件,通常来说我们编写的类运行时都是通过这个加载器加载的。

ClassLoader cl = Hello.class.getClassLoader();
System.out.println(cl);
System.out.println(cl.getParent());
// 我自己编写的 Hello 类,在 classpath 下面,输出为
// sun.misc.Launcher$AppClassLoader@18b4aac2
// sun.misc.Launcher$ExtClassLoader@3cd1a2f1

自定义类加载器

通过继承 ClassLoader 类,然后覆盖 findClass 方法,即可完成自定义类加载器的工作。

// ClassLoader 中默认直接抛出异常,需要自己实现寻找 class 文件的逻辑
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

后面会具体实现一个加载器,这里不再多说。

双亲委托机制

有了类加载器,具体的加载机制是怎样的呢?什么是双亲委托机制呢?

下面是 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)) {
        // 1
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2
                    c = parent.loadClass(name, false);
                } else {
                    // 3
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 4
                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;
    }
}

首先检查是否已经加载,是的话就直接返回

如果父加载器不为空,则调用父加载器的 loadClass 方法

如果父加载器为空,则使用根加载器加载

如果没有加载成功,则调用自己的 findClass 方法

可以看出,所谓双亲委托,也叫父委托机制,即所有类的加载都会先委托给父加载器进行处理,当父加载器不能完成加载时,才会自己加载。

这样做的好处是什么呢?

在 Java 中类的唯一性并不只是通过包名和类名进行判断,还会根据类加载器是否为同一个来进行判断。

假设同一个类由不同的类加载器分别加载,得到的 Class 对象也是不同的。

相当于一个类加载器就是一个命名空间,不同命名空间下的相同的包名和类名的类也不是同一个类。

由于双亲委托机制,类的加载首先交给父加载器加载,如 String 类,不管交给哪个加载器加载,最终都会由 BootstrapClassLoader 加载,所以拿到的 String 类都是同一份。

如果某人破坏了双亲加载机制,制造了一个 String 类出来,但是跟类库中的 String 类也是不同的,因为类加载器是不同的,防止了安全问题。

还有就是隔离的作用,比如 Tomcat 中有多个应用,假设是相同的应用,类都是一样的,但是使用了不同的类加载器分别加载,就实现了多个应用的隔离。

破坏双亲加载

说到破坏双亲委托机制,也很简单,自定义 ClassLoader 的时候,同时覆盖 loadClass 方法,自己实现加载逻辑,从而破坏双亲加载的机制。

补充一点,类加载器的父子关系并不是使用继承实现的,而是使用组合的方式,即子类持有父加载器,比如 ExtClassLoader 的继承自 URLClassLoader,但实际的父加载器是 BootstrapClassLoader。

线程上下文类加载器

为什么需要线程上下文类加载器呢?

从 SPI(Service Provider Interface)机制说起,JDK 提供了很多 SPI 接口,常见的如 JDBC,第三方厂商只需要提供具体实现即可。JDBC 接口类,其中包括 DriverManager 类是由 BootstrapClassLoader 加载,当在 DriverManager 中生成 Connection 的实现类时,需要加载具体的实现类,而 BootstrapClassLoader 是加载不到这些实现类的,由于双亲委托机制,又不能委托给 AppClassLoader。

这时就需要线程上下文类加载器了,在 DriverManager 中获取到线程上下文类加载器,通常就是 AppClassLoader,于是在 DriverManager 中可以加载到第三方的实现类了。

System.out.println(Thread.currentThread().getContextClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2

线程上下文的获取方式如上面的代码所示,当没有显式的进行设置时,默认是与父线程保持相同的类加载器,而所有线程又是从 Main 线程派生出来,即 main 函数所在的线程,main 函数所在的类是 AppClassLoader 加载的,所以默认情况下获取到的是 AppClassLoader。

热加载示例

下面来简单实现一个热加载的例子,首先定义一个接口

public interface Info {
    void show();
}

main 函数里面循环调用 show 方法,开始的时候使用一个匿名实现类。

import java.nio.file.*;
import java.util.concurrent.TimeUnit;

public class App {

    // 匿名实现输出 main info
    private static Info info = () -> System.out.println("main info");

    public static void main(String[] args) throws Exception {

        // 创建守护线程,监听 class 文件更改
        Thread thread = new Thread(()-> startWatch());
        thread.setDaemon(true);
        thread.start();

        while (true) {
            info.show();
            TimeUnit.SECONDS.sleep(5);
        }
    }

    private static void startWatch() {
        final Path path = Paths.get("/Users/test/classes");
        try {
            WatchService watcher = FileSystems.getDefault().newWatchService();

            path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);

            while (true) {
                WatchKey key = watcher.take();
                for (WatchEvent<?> event : key.pollEvents()) {
                    if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                        // 监听 class 文件的修改事件
                        String fileName = event.context().toString();
                        if (fileName.endsWith(".class")) {
                            String className = fileName.split("[.]")[0];
                            reload(className);
                        }
                    }
                }
                key.reset();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void reload(String name) {
        MyClassLoader classLoader = new MyClassLoader();
        try {
            Class<?> clazz = Class.forName(name, true, classLoader);
            // 替换 info 的实现类
            info = (Info) clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

同时有一个守护线程,负责监听 /Users/test/classes 目录下面 class 文件的变化事件,当 class 文件发生修改,则使用自定义的加载器重新加载类,然后替换 info。

自定义的加载器实现如下,覆盖 findClass 方法即可,可以加载 /Users/test/classes 文件夹下的 class 文件。

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File("/Users/test/classes/" + name + ".class");
        try {
            // 将文件读入 byte 数组,篇幅问题,具体实现就不贴出来了
            byte[] bytes = getClassBytes(file);
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
}

当程序启动起来后,控制台会不断的进行输出。接下来在 /Users/test/classes 文件夹下编写一个 Info 接口的实现类,然后使用 javac 进行编译,生成 class 文件。

public class InfoImpl implements Info {
    @Override
    public void show() {
        System.out.println("another info");
    }
}

守护线程会监听到这个修改事件,然后进行加载和实例化,并将该实例赋值给 info,从而改变了 info 的输出内容,如下

main info
main info
main info
main info
another info
another info
another info
...

这个示例比较简单,以后看了 tomcat 的实现再来聊聊,如果有不对的地方请提出来哈。



Previous Post
Java 线程池源码分析
Next Post
Java 线程池介绍