跳转到内容
唯一赫兹
返回

JVM 类加载机制

类加载的流程

只有加载的读取二进制流部分和初始化部分能够被开发者控制,其他都由 JVM 控制用户能够自定义的部分

类加载器

类加载器的分类
类加载器的分类

双亲委派

类加载器有属于自己的命名空间,不同类加载器加载同一限定名得到的 Class 会被判定为不同的类,下面这段代码中 instanceof 返回 false 能够印证这一特性

// 自定义一个ClassLoader,重写loadClass方法
ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class";
            byte[] b;
            try (InputStream is = getClass().getResourceAsStream(fileName)) {
                if (is == null) return super.loadClass(name);
                b = is.readAllBytes();
            }
            return defineClass(name, b, 0, b.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(name);
        }
    }
};
Object obj = myLoader.loadClass("com.v1hz.learnjvm.A").getConstructor().newInstance();
System.out.println(obj.getClass());

// 前者由自定义的ClassLoader加载,后者由AppClassLoader加载,所以输出false
System.out.println(obj instanceof com.v1hz.learnjvm.A);Main.java

双亲委派机制保证以下两点

具体实现中 ClassLoader 之间并不是继承关系,而是通过 parent 变量定义父加载器,即通过组合的形式来组织。以下为 ClassLoader 的 loadClass 方法源码,重点看注释部分:

// ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);  // 查询目标类是否已经被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 优先委托父加载器来加载目标类,如果parent为null,则父加载器为BootstrapClassLoader
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            // 父加载器无法加载,再由自己加载目标类
            if (c == null) {
                c = findClass(name);
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}ClassLoader.java

破坏双亲委派

重写 loadClass 方法

上文中通过重写 loadClass 方法就已经实现了对双亲委派模型的破坏,因为双亲委派的逻辑就存在于 loadClass 方法中 2。在实际开发中,JVM 推荐用户重写 findClass 方法而不是 loadClass 方法,以此来遵守双亲委派模型。

将上文的代码改为重写 findClass 方法,就不会破坏双亲委派模型:

// 重写findClass方法
ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException { /*...*/ }
};
// 输出true
System.out.println(obj instanceof com.v1hz.learnjvm.A);Main.java

SPI 机制

以 JDBC 的数据库驱动管理器 DriverManager 为例,其目标是让用户只需要改动 URL 字符串就能连接多个数据库,所以 DriverManager 内部需要遍历当前应用拥有的所有数据库驱动,然后判断这个 Driver 能否处理这个 URL。

但是 DriverManager 是 JDK 中的类,由 BootstrapClassLoader 加载 3,而各个数据库厂商实现的 Driver 是引入的第三方依赖,由 AppClassLoader 加载。父加载器无法看见子加载器加载的类,要让 DriverManager 看见 AppClassLoader 加载的类,就需要引入 SPI 机制,绕过双亲委派的单向可见性。

下面是 DriverManager 中加载所有 Driver 的部分源码,以及实现 SPI 机制的 ServiceLoader 的 load 方法源码,重点看注释部分:

// DriverManager.java
private static void ensureDriversInitialized() {
    if (driversInitialized) {
        return;
    }
    synchronized (lockForInitDrivers) {
        if (driversInitialized) {  // 双重检查锁
            return;
        }
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty(JDBC_DRIVERS_PROPERTY);
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 通过 SPI 查找所有 java.sql.Driver 接口的实现
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                // 强制触发所有 JDBC Driver 的加载和初始化
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try {
                    while (driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                }
                return null;
            }
        });
        if (drivers != null && !drivers.isEmpty()) {
            String[] driversList = drivers.split(":");
            for (String aDriver : driversList) {
                try {
                    Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                }
            }
        }
        driversInitialized = true;
        println("JDBC DriverManager initialized");
    }
}DriverManager.java
// ServiceLoader.java
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前线程的上下文类加载器,通常是 AppClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}ServiceLoader.java

可以看到 SPI 的实现是通过获取当前线程的 ClassLoader 来拿到 AppClassLoader 的,从而实现让父加载器委派子加载器加载类。

Footnotes

  1. 验证包含多个步骤,分散在不同的阶段,写在连接部分只是方便展示;解析阶段可以发生在初始化前,也可以发生在初始化后。

  2. 由于在推出双亲委派模型的时候市场上已经有大量破坏双亲委派模型的代码了,所以 JVM 考虑到向下兼容,最终没有把 loadClass 改为 final 方法,而是增加了 findClass 方法作为补救措施。

  3. Java9+ 中将 ExtClassLoader 改为了 PlatformClassLoader,这里的 DriverManager 也从由 BootstrapClassLoader 加载改为了由 PlatformClassLoader 加载。



下一篇
AstroPaper 踩坑(2):开发环境正常但生产构建丢失文章?居然是时区在作祟...