图解JVM - 17.再谈类的加载器

20 阅读11分钟

1. 概述

类加载器是JVM执行类加载机制的前提。

ClassLoader的作用:

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。

1.1 类加载器的分类

Java类加载器按层次分为四类:

  1. 引导类加载器(Bootstrap ClassLoader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应用程序类加载器(Application ClassLoader)
  4. 用户自定义类加载器(Custom ClassLoader)

1.2 类加载器的必要性

类加载器的核心价值体现在:

  • 隔离性:不同加载器加载的类相互不可见
  • 灵活性:支持动态加载、热部署等特性
  • 安全性:防止核心类被篡改(如java.lang.String)
  • 多样性:支持从非标准来源加载类(如网络、加密文件)

1.3 命名空间

关键点:

  1. 每个类加载器拥有独立的命名空间
  2. 不同命名空间的类默认互不可见
  3. 唯一性判断标准:全限定名 + 类加载器实例

1.4 类加载机制的基本特征

三大核心特征:

  1. 全盘委托:优先委派父加载器
  2. 缓存机制:已加载类直接返回
  3. 防止重复:确保类唯一性

1.5 类加载器之间的关系

层次结构特点:

  • 父子关系通过组合(非继承)实现
  • Bootstrap是顶级父加载器(由C++实现)
  • 自定义加载器默认父级是AppClassLoader
  • 形成树状而非简单的链式结构

2. 类的加载器分类

2.1 引导类加载器(Bootstrap ClassLoader)

核心特征:

  • 唯一没有父加载器的类加载器
  • 加载路径:<JAVA_HOME>/lib目录下的核心类库
  • 无法通过Java代码直接获取实例(返回null)
// 示例:获取String类的加载器
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader); // 输出null

2.2 扩展类加载器(Extension ClassLoader)

关键细节:

  • 父加载器是Bootstrap ClassLoader
  • 加载路径:<JAVA_HOME>/lib/ext目录
  • 对应Java类:sun.misc.Launcher$ExtClassLoader
// 获取扩展类加载器
ClassLoader extClassLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extClassLoader); 
// 输出 sun.misc.Launcher$ExtClassLoader@xxxxxx

2.3 系统类加载器(Application ClassLoader)

主要特点:

  • 父加载器是ExtClassLoader
  • 加载路径:当前应用的classpath
  • 对应Java类:sun.misc.Launcher$AppClassLoader
// 获取系统类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
// 输出 sun.misc.Launcher$AppClassLoader@xxxxxx

2.4 用户自定义类加载器

public class MyClassLoader extends ClassLoader {
    private String classPath;

    @Override
    protected Class<?> findClass(String name) {
        byte[] classData = loadClassData(name);
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        // 实现自定义加载逻辑...
    }
}

自定义场景:

  1. 热部署实现
  2. 加密字节码保护
  3. 模块化加载
  4. 多版本共存

3. 测试不同的类的加载器

3.1 核心类库测试

测试代码:

public class ClassLoaderTest {
    public static void main(String[] args) {
        printClassLoader("java.lang.String");
        printClassLoader("java.util.HashMap");
        printClassLoader("java.sql.DriverManager");
    }

    private static void printClassLoader(String className) {
        try {
            Class<?> clazz = Class.forName(className);
            System.out.println(className + " 加载器: " 
                               + clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

java.lang.String 加载器: null
java.util.HashMap 加载器: null 
java.sql.DriverManager 加载器: null

结论验证:

  • 所有核心类库均由BootstrapClassLoader加载
  • 显示null是因为BootstrapClassLoader由C++实现,无法在Java层获取引用

3.2 扩展类库测试

测试步骤:

  1. 找到JRE扩展目录(示例路径:<font style="background-color:rgb(252, 252, 252);">$JAVA_HOME/jre/lib/ext</font>
  2. 查看dnsns.jar中的DNSNameService类
  3. 执行测试代码:
printClassLoader("sun.net.spi.nameservice.dns.DNSNameService");

运行结果:

sun.net.spi.nameservice.dns.DNSNameService 加载器: 
sun.misc.Launcher$ExtClassLoader@4eec7777

关键发现:

  • 扩展目录中的类由ExtClassLoader加载
  • 父加载器链:ExtClassLoader → BootstrapClassLoader

3.3 应用类库测试

测试代码:

public class MyApplicationClass {
    public static void main(String[] args) {
        // 测试自定义类
        printClassLoader("com.example.MyService");
        // 测试第三方库
        printClassLoader("org.apache.commons.lang3.StringUtils");
    }
}

运行结果示例:

com.example.MyService 加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
org.apache.commons.lang3.StringUtils 加载器: sun.misc.Launcher$AppClassLoader@18b4aac2

重要结论:

  • 所有classpath中的类(包括第三方JAR)都由AppClassLoader加载
  • 父加载器委托机制保证核心类不会被重复加载

3.4 自定义类加载测试

测试代码示例:

public class CustomLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader customLoader = new MyClassLoader();
        Class<?> clazz = customLoader.loadClass("com.example.Demo");
        System.out.println("自定义类加载器: " + clazz.getClassLoader());
    }
}

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) {
        // 实现自定义加载逻辑
        byte[] bytes = loadClassData(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

典型输出:

自定义类加载器: MyClassLoader@6d06d69c

核心要点:

  • 自定义加载器打破双亲委派时需要重写loadClass方法
  • 标准实现应该遵循双亲委派模型(重写findClass)

4. ClassLoader源码解析

4.1 ClassLoader的主要方法

核心方法源码解析:

  1. loadClass() 双亲委派实现
protected Class<?> loadClass(String name, boolean resolve) 
throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
        // 1.检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2.递归调用父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器无法完成加载
            }

            if (c == null) {
                // 3.调用findClass自行加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  1. findClass() 模板方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
  1. defineClass() 类定义核心
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError {
    return defineClass(null, b, off, len, null);
}

4.2 SecureClassLoader与URLClassLoader

关键设计:

  • SecureClassLoader 添加了权限控制
  • URLClassLoader 实现从URL路径加载类
  • Ext/AppClassLoader 都是URLClassLoader的子类

4.3 ExtClassLoader与AppClassLoader

// Launcher.java片段
static class ExtClassLoader extends URLClassLoader {
    public static ExtClassLoader getExtClassLoader() throws IOException {
        File[] dirs = getExtDirs();
        return new ExtClassLoader(dirs);
    }
}

static class AppClassLoader extends URLClassLoader {
    AppClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
}

关键区别:

特性ExtClassLoaderAppClassLoader
加载路径jre/lib/ext/*.jarclasspath路径
父加载器BootstrapClassLoaderExtClassLoader
实现类sun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoader

4.4 Class.forName()与ClassLoader.loadClass()

代码对比:

// 默认执行初始化
Class.forName("com.example.Demo");

// 等效代码
Class.forName("com.example.Demo", true, currentClassLoader);

// 不执行初始化
classLoader.loadClass("com.example.Demo", false);

关键差异表:

特性Class.forName()ClassLoader.loadClass()
初始化触发默认触发默认不触发
异常处理抛出ClassNotFoundException需要手动处理异常
使用场景加载驱动类等需要初始化的场景类结构分析等不需要初始化的场景

5. 双亲委派模型

5.1 定义与本质

本质特征:

  1. 层级过滤机制:加载请求逐级过滤
  2. 优先权原则:父加载器优先尝试
  3. 安全屏障:防止核心类被篡改
  4. 类隔离基础:不同层级加载器形成隔离空间

常见误区:

  • 父加载器不是继承关系,而是组合关系
  • 不是严格的"双亲",而是单亲委派链
  • BootstrapClassLoader不参与Java层次结构

5.2 优势与劣势

优势总结:

  1. 安全性:防止篡改java.lang等核心类
  2. 高效性:避免重复加载消耗资源
  3. 稳定性:保证基础类行为一致
  4. 简单性:形成明确的类查找路径

劣势分析表:

劣势点具体表现典型案例
灵活性不足无法实现热替换Tomcat模块化加载
扩展性受限无法加载不同版本库OSGi模块系统
上下文隔离SPI服务加载困难JDBC驱动加载
性能损耗多级委派增加调用链路高频动态加载场景

5.3 破坏双亲委派机制

JDBC破坏案例解析:

// DriverManager静态代码块
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    // 使用线程上下文类加载器
    ServiceLoader<Driver> loadedDrivers = 
    ServiceLoader.load(Driver.class);
    // 加载实现类...
}

关键破坏手段:

  1. 线程上下文类加载器(Thread Context ClassLoader)
  2. 逆向委派:子加载器直接加载父加载器范围的类
  3. 分层加载:不同加载器加载相同类

5.4 热替换的实现

热替换实现代码框架:

public class HotSwap {
    private volatile Object serviceInstance;
    private ClassLoader currentLoader;

    public void init() throws Exception {
        File watchDir = new File("/hotswap");
        WatchService watcher = FileSystems.getDefault().newWatchService();
        watchDir.register(watcher, ENTRY_MODIFY);

        new Thread(() -> {
            while (true) {
                WatchKey key = watcher.take();
                reloadClass();
                key.reset();
            }
        }).start();
    }

    private void reloadClass() {
        ClassLoader newLoader = new HotClassLoader();
        Class<?> clazz = newLoader.loadClass("ServiceImpl");
        serviceInstance = clazz.newInstance();
        currentLoader = newLoader; // 切换引用
    }
}

热替换关键点:

  1. 使用自定义类加载器重新加载
  2. 新旧类必须实现相同接口
  3. 需要妥善处理静态状态迁移
  4. 旧类需满足卸载条件(无实例、无引用)

6. 沙箱安全机制

6.1 JDK1.0时期

初始安全模型特点:

  1. 非黑即白的二元安全模式
  2. 仅支持本地代码的完全信任
  3. 无细粒度权限控制
  4. 通过类加载器实现基础隔离

典型限制:

// 沙箱中运行的代码会抛出SecurityException
System.setSecurityManager(new SecurityManager());
FileInputStream fis = new FileInputStream("/etc/passwd"); // 抛出异常

6.2 JDK1.1时期

关键改进:

  1. 引入可扩展的安全管理器
  2. 细粒度权限检查方法
  3. 支持代码签名验证
  4. 实现策略文件雏形

权限配置文件示例(.java.policy):

policy


grant codeBase "http://example.com/-" {
    permission java.io.FilePermission "/tmp/*", "read";
    permission java.net.SocketPermission "*:80", "connect";
};

6.3 JDK1.2时期

安全架构升级要点:

  1. 引入保护域(ProtectionDomain)概念
  2. 实现权限(Permission)对象体系
  3. 建立访问控制器(AccessController)
  4. 支持动态权限判断

访问控制流程:

AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
        // 受信任代码块
        new File("/secret.txt").delete();
        return null;
    }
});

6.4 JDK1.6时期

最终形态安全特性:

  1. 支持主体(Principal)授权
  2. 增强的策略动态刷新能力
  3. 改进的权限继承机制
  4. 细粒度访问控制上下文

安全策略配置示例:

// 创建自定义权限
public class DatabasePermission extends BasicPermission {
    public DatabasePermission(String name) {
        super(name);
    }
}

// 策略文件配置
grant principal javax.security.auth.x500.X500Principal "CN=admin" {
    permission com.example.DatabasePermission "query";
};

安全机制执行流程:

7. 自定义类的加载器

7.1 为什么要自定义类加载器?

典型应用场景分析:

  1. 热部署/热更新

  1. 字节码加密保护
public class SecureClassLoader extends ClassLoader {
    protected Class<?> findClass(String name) {
        byte[] encryptedBytes = loadEncryptedClass(name);
        byte[] decrypted = decrypt(encryptedBytes); // 自定义解密算法
        return defineClass(name, decrypted, 0, decrypted.length);
    }
}
  1. 多版本共存

  1. 非标准来源加载
    • 数据库存储的类字节码
    • 网络动态传输的类文件
    • 内存实时生成的字节码

7.2 实现方式

标准实现模板

分步实现指南:

  1. 继承ClassLoader基类
public class MyClassLoader extends ClassLoader {
    public MyClassLoader(ClassLoader parent) {
        super(parent); // 显式指定父加载器
    }
}
  1. 重写findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classBytes = loadClassBytes(name);
    return defineClass(name, classBytes, 0, classBytes.length);
}
  1. 实现字节码加载
private byte[] loadClassBytes(String className) {
String path = className.replace('.', '/') + ".class";
try (InputStream ins = new FileInputStream(path);
     ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    int bufferSize = 4096;
    byte[] buffer = new byte[bufferSize];
    int bytesNumRead;
    while ((bytesNumRead = ins.read(buffer)) != -1) {
        baos.write(buffer, 0, bytesNumRead);
    }
    return baos.toByteArray();
} catch (IOException e) {
    throw new ClassNotFoundException(className, e);
}
}
  1. 打破双亲委派(可选)
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
    // 针对特定包名跳过双亲委派
    if (name.startsWith("com.example.special")) {
        return findClass(name);
    }
    return super.loadClass(name, resolve);
}

线程上下文类加载器

使用模式:

// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(myClassLoader);

// 获取上下文类加载器
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> clazz = loader.loadClass("com.example.Service");

典型面试实现题

需求:实现热部署类加载器

public class HotswapClassLoader extends ClassLoader {
    private String basePath;

    public HotswapClassLoader(String basePath) {
        super(ClassLoader.getSystemClassLoader().getParent());
        this.basePath = basePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String name) {
        // 从指定路径加载最新字节码
        String path = basePath + name.replace('.', '/') + ".class";
        try (InputStream ins = new FileInputStream(path)) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = ins.read()) != -1) {
                baos.write(b);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

8. Java9新特性

8.1 模块化系统对类加载的影响

重大变化:

  1. 引导类加载器可见性:BootstrapClassLoader在Java层可见(仍由C++实现)
  2. 平台类加载器:替代原来的ExtClassLoader
  3. 模块化层次结构

  1. 资源访问限制:模块需显式暴露资源

8.2 类加载器API变化

// 模块化后的类加载示例
ModuleLayer.Controller controller = ModuleLayer.defineModulesWithOneLoader(
    configuration,
    List.of(parentLayer), 
    ClassLoader.getSystemClassLoader()
);

Class<?> clazz = controller.layer().findLoader("com.module")
.loadClass("com.example.Service");

新增核心类:

  • <font style="background-color:rgb(252, 252, 252);">java.lang.Module</font>
  • <font style="background-color:rgb(252, 252, 252);">java.lang.ModuleLayer</font>
  • <font style="background-color:rgb(252, 252, 252);">java.lang.Configuration</font>

8.3 兼容性处理

迁移策略:

  1. 使用<font style="background-color:rgb(252, 252, 252);">--add-exports</font>暴露内部API
  2. <font style="background-color:rgb(252, 252, 252);">--add-opens</font>允许反射访问
  3. <font style="background-color:rgb(252, 252, 252);">--patch-module</font>修补模块

(第八部分内容结束,后续将进入"常见问题与解决方案"章节)


9. 常见问题与解决方案

9.1 NoClassDefFoundError vs ClassNotFoundException

解决方案:

// ClassNotFoundException处理示例
try {
    Class<?> clazz = Class.forName("com.example.MissingClass");
} catch (ClassNotFoundException e) {
    // 检查classpath配置
    // 验证依赖是否完整
    // 确认类名拼写
}

// NoClassDefFoundError预防措施
public class SafeInit {
    static {
        try {
            // 避免静态块抛出异常
            init();
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}

9.2 实现热替换的难点

常见问题:

  1. 静态状态迁移困难
  2. 旧实例无法自动升级
  3. 资源句柄无法转移

解决方案:

  • 使用接口隔离实现
  • 通过代理模式切换实现
  • 结合OSGi等成熟框架

(第九部分内容结束,最后将呈现"高频面试问题与解答")


10. 高频面试问题与解答

10.1 双亲委派机制的优势与不足

问题:请解释双亲委派模型的工作原理及其优缺点

解答

1. 工作原理(图示辅助理解)

  1. 三大优势
    • 安全性:防止核心类库被篡改
    • 唯一性:避免重复加载消耗内存
    • 高效性:父加载器缓存提升加载速度
  2. 主要缺陷
    • 上下文隔离导致SPI加载困难
    • 无法实现真正的热替换
    • 模块化场景适应性差

10.2 如何打破双亲委派机制

问题:列举三种打破双亲委派模型的场景及实现方式

解答

1. 线程上下文加载器

// JDBC驱动加载案例
ClassLoader original = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(customLoader);
    DriverManager.getConnection(url); // 驱动使用上下文加载器
} finally {
    Thread.currentThread().setContextClassLoader(original);
}
  1. OSGi模块化方案
    • 每个Bundle使用独立类加载器
    • 网状结构的类依赖关系
  2. Tomcat容器设计
    • Web应用隔离:每个WAR使用独立加载器
    • 共享库统一加载:/common目录
    • 热部署实现:创建新加载器实例