类加载机制
在讲动态加载之前,必须先理解 Java 的类加载机制,Java 类的加载不是在编译时完成的,而是在运行时由 类加载器 负责的。
- 加载:查找并加载类的二进制数据(
.class文件)。 - 链接:
- 验证:确保
.class文件的正确性。 - 准备:为类的静态变量分配内存,并初始化默认值。
- 解析:将常量池中的符号引用替换为直接引用。
- 验证:确保
- 初始化:执行类的静态初始化块和静态变量的赋值。
Java 默认有三种类加载器,它们形成了双亲委派模型:
- Bootstrap ClassLoader (启动类加载器):加载 Java 核心库 (
JAVA_HOME/jre/lib/rt.jar),它是 C++ 实现的,不是ClassLoader的子类。 - Extension ClassLoader (扩展类加载器):加载 Java 扩展库 (
JAVA_HOME/jre/lib/ext/)。 - Application ClassLoader (应用程序类加载器):加载应用程序的类路径 (
classpath) 下的类,我们通常使用的Class.forName()和ClassLoader.getSystemClassLoader()默认都是它。
双亲委派模型:当一个类加载器收到加载请求时,它首先会把这个请求委派给父类加载器,直到顶层的启动类加载器,如果父类加载器无法完成加载(在其搜索范围内没找到),子加载器才会尝试自己去加载。
这个模型的好处是安全性,可以防止核心 API 被随意替换。
动态加载 Class 的主要方式
动态加载的核心是获取一个 ClassLoader 实例,然后调用其 loadClass() 方法,这与 Class.forName() 有本质区别,我们后面会讲。
使用 ClassLoader.loadClass() (最核心的方式)
这是最直接、最灵活的动态加载方式,它允许你指定使用哪个类加载器来加载类,并且不会自动初始化该类(除非设置了 resolve 参数)。
代码示例:
假设我们有一个简单的类 MyService,它位于 com.example 包下,并且编译后的 .class 文件在 D:/temp/classes 目录中。
MyService.java
package com.example;
public class MyService {
static {
System.out.println("MyService 静态代码块被初始化了!");
}
public void sayHello() {
System.out.println("Hello from MyService!");
}
}
DynamicLoaderDemo.java
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class DynamicLoaderDemo {
public static void main(String[] args) throws Exception {
// 1. 定义存放 .class 文件的目录
String classPath = "D:/temp/classes";
File file = new File(classPath);
// 2. 创建一个 URLClassLoader,它可以从指定的文件系统路径加载类
URL url = file.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
// 3. 动态加载类 (这里使用 loadClass)
// 第一个参数是类的全限定名
// 第二个参数 `false` 表示不解析(即不链接),通常我们只需要加载,后续通过反射使用时会自动链接和初始化
Class<?> clazz = classLoader.loadClass("com.example.MyService");
System.out.println("类加载器: " + clazz.getClassLoader());
System.out.println("类是否被初始化: " + (clazz.getDeclaredField("initialized") != null)); // 这行只是示例,实际判断初始化更复杂
// 4. 使用反射创建实例并调用方法
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(instance);
// 关闭类加载器(可选,但推荐)
classLoader.close();
}
}
如何运行:
- 编译
MyService.java:javac -d D:/temp/classes MyService.java - 运行
DynamicLoaderDemo.java:java DynamicLoaderDemo
输出:
类加载器: sun.net.www.protocol.file.FileURLClassLoader@xxxxx
Hello from MyService!
注意:你会发现 MyService 的静态代码块没有被执行,这是因为 loadClass() 默认不会初始化类,当你第一次调用 clazz.newInstance() (在 Java 9+ 中是 getDeclaredConstructor().newInstance()) 或者访问类的静态变量/方法时,JVM 才会触发类的初始化。
使用 Class.forName() (经典方式)
Class.forName() 是另一种常用的方式,但它与 loadClass() 有显著区别。
Class.forName(String className):
- 默认使用当前类的类加载器来加载类。
- 加载完成后,会立即初始化该类(执行静态代码块)。
代码示例:
public class ForNameDemo {
public static void main(String[] args) throws Exception {
// 默认使用调用 ForNameDemo 这个类的类加载器(通常是AppClassLoader)
// 并且会立即初始化 MyService
Class<?> clazz = Class.forName("com.example.MyService");
System.out.println("类加载器: " + clazz.getClassLoader());
// 因为上面已经初始化了,所以这里直接创建实例即可
Object instance = clazz.getDeclaredConstructor().newInstance();
instance.getClass().getMethod("sayHello").invoke(instance);
}
}
输出:
MyService 静态代码块被初始化了!
类加载器: sun.misc.Launcher$AppClassLoader@xxxxx
Hello from MyService!
可以看到,静态代码块输出了,说明类被初始化了。
Class.forName(String className, boolean initialize, ClassLoader loader):
这个重载版本结合了两者的优点,非常强大。
className: 类的全限定名。initialize: 是否初始化该类,设为false时,行为与loadClass()类似。- `loader 指定使用哪个类加载器**来加载类。
代码示例:
public class ForNameWithLoaderDemo {
public static void main(String[] args) throws Exception {
String classPath = "D:/temp/classes";
File file = new File(classPath);
URL url = file.toURI().toURL();
URLClassLoader customClassLoader = new URLClassLoader(new URL[]{url});
// 使用自定义类加载器,并且不进行初始化
Class<?> clazz = Class.forName("com.example.MyService", false, customClassLoader);
System.out.println("类加载器: " + clazz.getClassLoader());
System.out.println("类是否被初始化: " + (clazz.getDeclaredField("initialized") != null)); // 同样,这只是示意
// 当我们创建实例时,才会触发初始化
Object instance = clazz.getDeclaredConstructor().newInstance();
instance.getClass().getMethod("sayHello").invoke(instance);
customClassLoader.close();
}
}
这个版本提供了最大的灵活性,既可以指定类加载器,又可以控制是否初始化。
使用 ServiceLoader (Java SPI 机制)
这是 Java 标准库提供的一种服务发现机制,常用于驱动加载(如 JDBC),它背后也使用了自定义的类加载器。
场景:你定义一个服务接口,第三方可以提供该接口的实现,你的程序可以在运行时动态发现并加载这些实现,而无需在编译时依赖它们。
代码示例:
-
定义服务接口
src/main/java/com/example/MyServiceInterface.javapackage com.example; public interface MyServiceInterface { void execute(); } -
提供服务实现 假设我们有两个不同的实现,放在不同的 jar 包或目录中。
impl1/src/main/java/com/example/impl/MyServiceImpl1.javapackage com.example.impl; import com.example.MyServiceInterface; public class MyServiceImpl1 implements MyServiceInterface { @Override public void execute() { System.out.println("Executing MyServiceImpl1"); } }impl2/src/main/java/com/example/impl/MyServiceImpl2.javapackage com.example.impl; import com.example.MyServiceInterface; public class MyServiceImpl2 implements MyServiceInterface { @Override public void execute() { System.out.println("Executing MyServiceImpl2"); } } -
创建服务配置文件 在每个实现的
META-INF/services/目录下创建一个文件,文件名是服务接口的全限定名。impl1/META-INF/services/com.example.MyServiceInterface文件内容:com.example.impl.MyServiceImpl1impl2/META-INF/services/com.example.MyServiceInterface文件内容:com.example.impl.MyServiceImpl2 -
使用 ServiceLoader 加载
src/main/java/com/example/ServiceLoaderDemo.javaimport com.example.MyServiceInterface; import java.util.ServiceLoader; public class ServiceLoaderDemo { public static void main(String[] args) { // ServiceLoader 会使用上下文类加载器来查找配置文件并加载实现类 ServiceLoader<MyServiceInterface> loader = ServiceLoader.load(MyServiceInterface.class); System.out.println("Found service implementations:"); for (MyServiceInterface service : loader) { service.execute(); } } }如何运行:你需要将
impl1.jar和impl2.jar添加到 classpath 中运行。ServiceLoader会自动扫描所有 jar 包中的META-INF/services目录,找到所有匹配的实现。
| 特性 | ClassLoader.loadClass() |
Class.forName() |
|---|---|---|
| 初始化 | 默认不初始化,延迟到第一次使用时。 | 默认会立即初始化。 |
| 类加载器 | 可以明确指定使用哪个 ClassLoader。 |
默认使用调用该方法的类的类加载器。 |
| 灵活性 | 非常高,是底层动态加载的基础。 | 相对较低,但 forName(name, init, loader) 重载版本也很灵活。 |
| 主要用途 | 框架、插件系统、热部署,需要精确控制加载过程和类加载器隔离的场景。 | JDBC 加载驱动、ORM 框架、通用反射工具,通常希望类加载后立即可用。 |
应用场景
- 插件化架构:应用程序可以动态加载位于特定目录(如
plugins/)的 jar 包,实现功能的扩展和更新,而无需重新主程序。 - 热部署:在开发或运行时,替换掉旧的
.class文件,然后通过新的类加载器加载新类,实现不重启应用更新代码。 - 框架集成:
- Spring:大量使用
Class.forName()加载配置、注解等。 - MyBatis/JDBC:使用
Class.forName()加载数据库驱动类。 - 各种 SPI:使用
ServiceLoader加载日志实现、数据库方言等。
- Spring:大量使用
- 测试工具:如 JUnit、Mockito,需要动态加载用户编写的测试类。
注意事项
- 类加载器隔离:不同类加载器加载的类,即使全限定名相同,在 JVM 中也被视为不同的类,它们不能直接相互转换,也无法访问对方的私有成员,这是实现插件隔离的关键。
- 内存泄漏:类加载器及其加载的所有类都会占用内存,在热部署或插件卸载时,如果不正确地关闭和释放类加载器,可能会导致内存泄漏。
URLClassLoader实现了AutoCloseable,建议使用try-with-resources。 - 安全性:动态加载代码可能带来安全风险,确保加载的来源是可信的。
- Java 模块系统 (JPMS): 在 Java 9+ 的模块化项目中,类加载机制变得更加复杂。
URLClassLoader的使用受到限制,推荐使用ModuleLayer和ServiceLoader来实现更安全的动态加载。
