在 Java 中,“动态加载”通常指的是在程序运行时,而不是在编译时,去查找并加载一个类文件(.class),这个过程由 Java 虚拟机 的类加载子系统完成。

核心概念:类加载器
理解动态加载的关键在于理解 Java 的类加载器,类加载器负责将类的 .class 文件从文件系统、网络或其他来源读取到内存中,并将其转换为 java.lang.Class 对象,Java 主要有三种内置的类加载器,它们形成了父子层级关系:
-
Bootstrap ClassLoader (启动类加载器)
- 负责加载 Java 核心库 (
java.lang.*,java.util.*等),位于JAVA_HOME/jre/lib目录下。 - 它是 C++ 实现的,是所有类加载器的顶层,没有父加载器。
- 负责加载 Java 核心库 (
-
Extension ClassLoader (扩展类加载器)
- 负责加载 Java 扩展库,位于
JAVA_HOME/jre/lib/ext目录下的.jar文件。 - 它的父加载器是 Bootstrap ClassLoader。
- 负责加载 Java 扩展库,位于
-
Application ClassLoader (应用程序类加载器 / 系统类加载器)
(图片来源网络,侵删)- 负责加载应用程序的类路径 (
classpath) 下的类,也就是我们平时开发中大部分的类。 - 它的父加载器是 Extension ClassLoader。
- 我们可以通过
ClassLoader.getSystemClassLoader()获取到它。
- 负责加载应用程序的类路径 (
双亲委派模型:当一个类加载器收到加载请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的 Bootstrap ClassLoader,只有当父加载器反馈自己无法完成加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。
动态加载 Class 的三种主要方式
以下是三种最常用的动态加载 Class 的方法,它们各有特点。
Class.forName(String className)
这是最经典、最常用的动态加载方式。
-
描述:这是一个静态方法,它接收一个类的全限定名(
"java.lang.String"或"com.example.MyClass"),并返回对应的Class对象。 -
工作原理:
- 它会调用当前类的默认类加载器(通常是调用该方法的类的类加载器,或者更常见的是应用程序类加载器)来加载类。
- 加载类的过程包括:加载、链接(验证、准备、解析)和初始化。
- 关键点:
forName会触发类的初始化,类的初始化是指执行类中的静态变量赋值和静态代码块(static{})。
-
示例:
public class ForNameExample { public static void main(String[] args) { try { // 动态加载一个类 String className = "java.util.ArrayList"; Class<?> clazz = Class.forName(className); System.out.println("成功加载类: " + clazz.getName()); System.out.println("类加载器: " + clazz.getClassLoader()); // 获取类的实例并调用方法 Object instance = clazz.getDeclaredConstructor().newInstance(); System.out.println("成功创建实例: " + instance); } catch (ClassNotFoundException e) { System.err.println("找不到指定的类: " + e.getMessage()); } catch (Exception e) { e.printStackTrace(); } } } -
优点:
- 简单直接,是 Java 反射机制的核心。
- 能够确保类被正确初始化。
-
缺点:
- 会触发类的初始化,这在某些场景下可能是不希望发生的(一个类的静态代码块非常耗时或有副作用)。
- 加载器不够灵活,总是使用调用者或系统的类加载器。
ClassLoader.loadClass(String name)
这是从类加载器实例本身调用的加载方法。
-
描述:这是一个实例方法,需要先获取到一个
ClassLoader对象,然后通过它来加载类。 -
工作原理:
- 它遵循双亲委派模型,它会先请求父加载器加载,如果父加载器无法加载,再由自己加载。
- 关键点:
loadClass默认不会触发类的初始化,它只完成加载和链接步骤,如果你需要初始化,需要显式调用Class.initialize()方法。
-
示例:
public class LoadClassExample { public static void main(String[] args) { // 获取系统类加载器 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); try { // 使用类加载器加载类 String className = "java.util.HashMap"; Class<?> clazz = classLoader.loadClass(className); System.out.println("成功加载类: " + clazz.getName()); System.out.println("类加载器: " + clazz.getClassLoader()); // 注意:此时类可能没有被初始化 // 如果需要初始化,需要手动调用 // clazz.initialize(); // 获取实例 Object instance = clazz.getDeclaredConstructor().newInstance(); System.out.println("成功创建实例: " + instance); } catch (ClassNotFoundException e) { System.err.println("找不到指定的类: " + e.getMessage()); } catch (Exception e) { e.printStackTrace(); } } } -
优点:
- 更灵活,可以指定使用哪个类加载器。
- 默认不进行初始化,提供了更细粒度的控制。
-
缺点:
代码稍显繁琐,需要先获取类加载器实例。
自定义类加载器
这是最强大、最灵活的动态加载方式,允许你从任何来源(不仅仅是文件系统)加载类。
-
描述:通过继承
java.lang.ClassLoader类并重写findClass()方法来实现。 -
工作原理:
- 你需要实现自己的类加载逻辑,比如从网络 URL、数据库、加密的
.class文件或自定义的文件格式中读取字节码。 - 在
findClass方法中,你负责将字节数组转换为Class对象,通常使用defineClass(String name, byte[] b, int off, int len)方法来完成这个转换。 - 关键点:自定义类加载器通常不重写
loadClass方法,以保持双亲委派模型的完整性,你只需要重写findClass,让它在父加载器无法找到类时,再由你的逻辑去查找。
- 你需要实现自己的类加载逻辑,比如从网络 URL、数据库、加密的
-
示例:从一个自定义的目录
./my_classes/加载类。import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; // 自定义类加载器 public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 将 com.example.MyClass 转换为文件路径 my_classes/com/example/MyClass.class String path = classPath + File.separator + name.replace('.', File.separatorChar) + ".class"; byte[] classBytes = null; try (FileInputStream fis = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { baos.write(buffer, 0, len); } classBytes = baos.toByteArray(); } catch (IOException e) { throw new ClassNotFoundException("Class " + name + " not found at path: " + path, e); } if (classBytes == null) { throw new ClassNotFoundException("Class " + name + " not found."); } // 使用 defineClass 将字节数组转换为 Class 对象 return defineClass(name, classBytes, 0, classBytes.length); } public static void main(String[] args) { // 假设 MyPlugin.class 文件放在 ./my_classes/com/example/ 目录下 MyClassLoader myClassLoader = new MyClassLoader("./my_classes"); try { // 注意:这里使用 loadClass 不会初始化类 String className = "com.example.MyPlugin"; Class<?> clazz = myClassLoader.loadClass(className); System.out.println("成功加载类: " + clazz.getName()); System.out.println("类加载器: " + clazz.getClassLoader()); // 输出会是 com.example.MyClassLoader // 如果需要初始化,可以调用 // clazz.initialize(); // 创建实例 Object instance = clazz.getDeclaredConstructor().newInstance(); System.out.println("成功创建实例: " + instance); } catch (Exception e) { e.printStackTrace(); } } } -
优点:
- 极高的灵活性,可以控制类的来源和加载过程。
- 实现热部署、插件化架构、从网络加载代码等高级功能的基础。
-
缺点:
- 实现复杂,需要处理字节码和 I/O 异常。
- 如果破坏双亲委派模型,可能会带来安全和管理上的问题。
总结与对比
| 特性 | Class.forName() |
ClassLoader.loadClass() |
自定义 ClassLoader |
|---|---|---|---|
| 调用方式 | 静态方法 Class.forName("全限定名") |
实例方法 loader.loadClass("全限定名") |
继承 ClassLoader,重写 findClass() |
| 默认类加载器 | 调用者或系统类加载器 | 指定的 ClassLoader 实例 |
自定义的加载器 |
| 是否初始化类 | 是(默认行为) | 否(需手动调用 initialize()) |
否(需手动调用 initialize()) |
| 灵活性 | 较低 | 中等 | 极高 |
| 主要用途 | 数据库驱动加载、Spring 框架等,需要确保类被正确初始化的场景。 | 需要指定类加载器或避免类初始化的场景。 | 热部署、插件系统、从网络/数据库加载代码、代码隔离等。 |
典型应用场景
-
JDBC 驱动加载:
- 在 JDBC 4.0 之前,我们需要手动加载驱动:
Class.forName("com.mysql.jdbc.Driver");,这会触发驱动的静态代码块,从而向DriverManager注册自己。forName的“初始化”特性在这里是必需的。
- 在 JDBC 4.0 之前,我们需要手动加载驱动:
-
Spring Framework:
Spring 大量使用反射来动态创建和管理 Bean,它会使用类加载器来加载配置文件中定义的类,并根据需要决定是否进行初始化。
-
插件化架构:
像 Eclipse、IntelliJ IDEA 这样的 IDE,允许用户安装插件,每个插件通常运行在它自己的自定义类加载器中,这样可以实现插件间的隔离,并且可以在不重启主程序的情况下卸载和加载新插件(热部署)。
-
代码热部署:
- 在一些开发服务器(如 JRebel)中,通过自定义类加载器,可以在检测到
.class文件更新后,加载新的类定义,而旧的类实例仍然存在,从而实现不重启应用就能更新代码的效果。
- 在一些开发服务器(如 JRebel)中,通过自定义类加载器,可以在检测到
-
从网络加载类:
在 P2P 或分布式系统中,可以将一个任务的代码(一个或多个类)作为字节码通过网络传输,然后在接收方通过自定义类加载器动态加载并执行。
选择哪种方式取决于你的具体需求:是简单加载,还是需要精细控制加载过程,或是实现复杂的动态系统。
