JVM 之 类加载器

摘要

类加载器

  • 左侧是JDK中实现的类加载器,通过parent属性形成⽗⼦关系。应⽤中⾃定义的类加载器的parent都是
    AppClassLoader

  • 右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现⾃定义类
    加载器。

  • 在代码中查看类加载器关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoaderDemo {
public static void main(String[] args) throws ClassNotFoundException {
// ⽗⼦关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
ClassLoader cl1 = LoaderDemo.class.getClassLoader();
System.out.println("cl1 > " + cl1); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("parent of cl1 > " + cl1.getParent()); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// BootStrap Classloader由C++开发,是JVM虚拟机的⼀部分,本身不是JAVA类。
System.out.println("grant parent of cl1 > " + cl1.getParent().getParent()); // null
// String,Int等基础类由BootStrap Classloader加载。
ClassLoader cl2 = String.class.getClassLoader();
System.out.println("cl2 > " + cl2); // null
System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader()); // null
}
}

双亲委派机制

  • 当⼀个类加载器要加载⼀个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。

  • Java 类加载机制中的双亲委派模型(Parent Delegation Model)是一种保证了类加载器按照层次结构从上到下来加载类的策略。这种层级化的加载流程确保了应用程序能够安全地加载并使用来自不同来源的类,同时也避免了内存中出现相同类的多个拷贝。

  • 以下是双亲委派机制的工作原理:

    • 1.当一个类加载器接收到类加载请求时,它首先不会自行尝试去寻找类文件,而是将这个请求委派给它的父类加载器。
    • 2.父类加载器同样遵循此规则,它会继续把请求向上委派给它的父类加载器,直到达到根(bootstrapp)类加载器为止。
    • 3.根类加载器一般会直接访问本地文件系统来查找类文件,比如 JDK 自带的核心类库,或者在-Xbootclasspath指定的路径下查找。
    • 4.如果根类加载器找到了该类,则进行类的加载;如果找不到,则把这个任务交回给发出请求的子类加载器。
    • 5.子类加载器也重复步骤 4,若找到则加载,否则传递给下一个子类加载器,直至原始提出请求的类加载器。
    • 6.若所有的类加载器都未能找到所需的类,则最终抛出ClassNotFoundException异常。
  • 双亲委派有以下几个优点:

    • 安全性:由于类加载是从顶层开始,这能防止恶意代码通过加载相同的包名和类名替代系统的关键类。
    • 唯一性:每个类都会被特定的类加载器加载一次,即便是在分布式的环境中也能保证类的统一性,避免因为多次加载导致的错误。
    • 可靠性:用户自定义的类加载器不用担心基础类已经被加载,它们可以专心于自己需要处理的部分。
  • 例如,当应用程序运行时,应用类加载器(Application ClassLoader)接收到对java.lang.String类的加载请求时,它会首先将请求传递给扩展类加载器(Extension ClassLoader),后者再传递给引导类加载器(Bootstrap ClassLoader)。引导类加载器会在其搜索路径中找到String类,并完成加载过程。如果应用程序试图提供自己的String类,由于双亲委派的存在,应用程序所指定的类并不会被加载,从而保证了平台核心 API 的一致性。

  • 总之,双亲委派机制是 Java 类加载过程中一个非常重要的特性,它不仅维护了类加载的安全性和一致性,也为开发者提供了灵活定制类加载规则的能力。

每个类加载器查找类的默认路径

  • 在 Java 中,每个类加载器都有自己的类路径(Classpath)去查找类文件。下面是几个主要的类加载器以及它们的默认查找路径:

类加载器名称 类名 / 别名 父类加载器 加载内容描述 默认查找路径 / 系统属性
启动类加载器 BootstrapClassLoader 加载 核心 Java 类库 <JAVA_HOME>/jre/lib/rt.jar(JDK8 及之前)或 sun.boot.class.path 指定路径
扩展类加载器 ExtClassLoader BootstrapClassLoader 加载 Java 平台扩展类库 <JAVA_HOME>/jre/lib/ext 或由 java.ext.dirs 系统属性指定
应用类加载器 AppClassLoader ExtClassLoader 加载 应用程序类(用户编写的代码) -classpath-cp 参数、CLASSPATH 环境变量、java.class.path 属性、当前目录 (.)
自定义类加载器 用户自定义 可指定 可通过继承 ClassLoader 并重写 findClass() 方法实现自定义类加载逻辑,如从网络、数据库等加载类文件 默认继承父加载器查找路径,也可自定义
  • 需要注意的是,除了 Bootstrap ClassLoader 外,其他的所有类加载器最终都是java.lang.ClassLoader的子类,并且每个类加载器实例都有一个直接的父类加载器。如果你创建了一个新的类加载器,它将继承 Application ClassLoader 作为它的父类,除非你在创建时指定了不同的父类加载器。

  • 此外,Java 9 引入了模块化系统后,类加载机制也有了一些变化,对于模块路径上的类加载,会使用新的层次结构来处理,这使得类加载过程更加灵活同时保持了向后的兼容性。不过,对于传统的类路径上的类加载,双亲委派模型仍然适用。

类加载器名称 类名 / 别名 父类加载器 加载模块范围 默认查找路径 / 模块来源
引导类加载器 BootstrapClassLoader 加载 java.* 模块,例如 java.base(包含 java.lang, java.util 等) $JAVA_HOME/jmods 中的模块文件,核心模块由 JVM 启动时直接加载
平台类加载器 PlatformClassLoader BootstrapClassLoader 加载 jdk.*javafx.* 等平台扩展模块 $JAVA_HOME/jmods,模块名为 jdk.*javafx.* 等;
应用类加载器 AppClassLoader PlatformClassLoader 加载应用模块(用户代码),包括模块路径 --module-path 和类路径 -classpath 下的类 应用程序编写的模块或类,来自命令行 --module-path-classpath 参数、CLASSPATH 环境变量等
自定义类加载器 用户自定义 可指定父加载器 加载非标准位置的类,可用于插件、网络加载、加密类加载等 默认遵循双亲委派,可重写 findClass() 实现自定义路径查找或其他数据源,如网络、数据库、加密文件等

jdk9+中,在委派给父加载器加载前,先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

  • 在代码中查看类加载路径

1
2
3
4
5
6
// BootStrap Classloader,加载java基础类。
System.out.println("BootStrap ClassLoader加载⽬录:" + System.getProperty("sun.boot.class.path"));
// Extention Classloader 加载⼀些扩展类。 可通过-D java.ext.dirs另⾏指定⽬录
System.out.println("Extention ClassLoader加载⽬录:" + System.getProperty("java.ext.dirs"));
// AppClassLoader 加载CLASSPATH,应⽤下的Jar包。可通过-D java.class.path另⾏指定⽬录
System.out.println("AppClassLoader加载⽬录:" + System.getProperty("java.class.path"));

双亲委派机制的实现原理

  • java.lang.ClassLoader 类的 loadClass 方法是双亲委派的核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 加载指定名称的类,并根据 resolve 参数决定是否解析该类。
*
* 该方法实现了类加载的基本逻辑,包括检查类是否已加载、委托父类加载器加载、
* 自行加载类以及解析类等步骤。该方法是 Java 类加载机制的核心部分之一,
* 遵循双亲委派模型。
*
* @param name 要加载的类的全限定名
* @param resolve 如果为 true,则在加载后解析该类
* @return 加载的 Class 对象
* @throws ClassNotFoundException 如果找不到指定的类
*/
// 这个⽅法是protected声明的,意味着,是可以被⼦类覆盖的,所以,双亲委派机制也是可以被打破的,如Tomcat⼦类重写这个⽅法,并使⽤自己的类加载逻辑。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 使用 synchronized 确保多线程环境下类加载的同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 每个类加载器对他加载过的类都有⼀个缓存,先去缓存中查看有没有加载过
Class<?> c = findLoadedClass(name);
if (c == null) { //没有加载过,就⾛双亲委派
long t0 = System.nanoTime();
try {
// 父类存在则让⽗类加载器进⾏加载
if (parent != null) {
c = parent.loadClass(name, false);
} else { //如果父类不存在,则从引导类加载器进⾏加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// findClass 方法是子类实现的,用于加载指定名称的类
// ⽗类加载器没有加载过,就⾃⾏解析class⽂件加载
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;
}
}
  • 该方法实现了类加载的基本逻辑,包括检查类是否已加载、委托父类加载器加载、自行加载类以及解析类等步骤。该方法是 Java 类加载机制的核心部分之一,遵循双亲委派模型。

  • 这个⽅法是protected声明的,意味着,是可以被⼦类覆盖的,所以,双亲委派机制也是可以被打破的,如Tomcat⼦类重写这个⽅法,并使⽤自己的类加载逻辑。

沙箱保护机制

  • 沙箱保护机制是 Java 虚拟机提供的一种安全机制,用于保护应用程序免受恶意代码的攻击。

  • java.lang.ClassLoader 类的 preDefineClass 方法是沙箱保护机制的核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 这个方法在双亲委派模型之前被调用,用于在加载类之前进行一些预处理操作。
// 参数:
// name: 要加载的类的全限定名。
// pd: 提供的保护域信息,可能为 null。
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}

if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}
  • 这个方法 preDefineClass 的作用是在类被定义之前进行一些安全检查和准备工作,确保类的合法性与安全性。它通常用于自定义类加载器中,以增强类加载过程中的安全控制。

  • 方法作用详解:

    • 1.防止定义非法类名的类(如 java.* 包下的类):
      • 如果尝试加载的类属于 java. 开头的标准包(如 java.lang, java.util 等),会抛出 SecurityException。
      • 这是为了防止用户自定义类伪装成 Java 核心类库中的类,从而造成安全风险。
    • 2.校验类名合法性:
      • 调用 checkName(name) 检查类名是否合法(例如不能包含 /、非法字符等),若不合法则抛出 NoClassDefFoundError。
    • 3.证书一致性校验(签名一致性校验):
      • 如果类有名称且提供了 ProtectionDomain,会调用 checkCerts(name, codeSource) 来确保当前类的签名与其所在包中其他类的签名一致。
      • 防止同一包中混入不同签名的类,避免潜在的恶意篡改。
    • 4.设置默认保护域(ProtectionDomain):
      • 如果传入的 ProtectionDomain 为 null,则使用类加载器的默认域 defaultDomain。

Linking链接过程

  • 在ClassLoader的loadClass⽅法中,还有⼀个不起眼的步骤,resolveClass。这是⼀个native⽅法。⽽其实现的过程称为linking-链接

  • 链接过程的实现功能如下图:

  • 其中关于半初始化状态就是JDK在处理⼀个类的static静态属性时,会先给这个属性分配⼀个默认值,作⽤是占住内存。然后等连接过程完成后,在后⾯的初始化阶段,再将静态属性从默认值修改为指定的初始值。

  • 符号引⽤和直接引⽤

如果A类中有⼀个静态属性,引⽤了另⼀个B类。那么在对类进⾏初始化的过程中,因为A和B这两个类都没有初始化,JVM并不知道A和B这两个类的具体地址。所以这时,在A类中,只能创建⼀个不知道具体地址的引⽤,指向B类。这个引⽤就称为符号引⽤。⽽当A类和B类都完成初始化后,JVM⾃然就需要将这个符号引⽤转⽽指向B类具体的内存地址,这个引⽤就称为直接引⽤。

  • 来看一个有意思的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Apple {
// 静态变量按声明顺序初始化
// 构造方法初始化apple对象时,price还没有被初始化,处于链接过程中的准备阶段,即半初始化状态,所以price为默认值0.0
static Apple apple = new Apple(10);
// 解决方法是将price声明在apple的上面即可
static double price = 20.00;

double totalpay;

public Apple(double discount) {
System.out.println("====" + price);
totalpay = price - discount;
}
}

public class PriceTest {
public static void main(String[] args) {
System.out.println(Apple.apple.totalpay); // -10.0
}
}
  • 这里有一个有意思的问题,就是只有当loadClass方法中的resolve参数为trueresolveClass方法才会被调用,但是大部分情况下,resolve参数都是false,这不是强制限制而是出于以下原因:

      1. 避免递归解析中出现错误或死循环
        在类的解析过程中,如果该类引用了另一个还没加载的类,立即解析可能会导致无限递归或加载顺序问题。通过延迟解析,可以更好地控制加载流程。
      1. 提高加载效率
        加载类可能不一定马上就用到所有方法、字段等符号引用,推迟解析可以提高性能,尤其在批量加载很多类时。
      1. 更灵活地处理类的依赖
        开发者可以先加载类,稍后根据需要再解析。例如,在自定义类加载器中,可能先判断类是否已经存在、是否需要被增强(比如字节码增强),再决定是否解析。

通过类加载器引⼊外部Jar包

  • 虽然通常我们会将依赖的 jar 包直接放入项目的 classpath 中(比如通过构建工具如 Maven 或 Gradle 管理),但在某些特定场景下,我们确实需要动态地通过 URLClassLoader 加载外部 jar 包,这是 Java 提供的一种更灵活的类加载机制。

  • 以下是一些必须或推荐使用 URLClassLoader 的典型应用场景:

1. 插件机制(Plugin System)

  • 场景说明:
    系统支持用户自定义插件(如 IDE 插件、浏览器扩展、游戏 mod),这些插件在运行时才加载,项目本身在编译期并不知道有哪些插件。

  • 举例:
    Eclipse 或 IntelliJ 的插件系统
    Minecraft 的 mod 加载器
    Spring Boot Devtools 重新加载机制

  • 为什么不能直接放入 classpath?
    因为插件是动态发现和加载的,不是编译时确定的。

2. 热部署 / 动态加载类

  • 场景说明:
    你想在应用运行过程中加载新的 jar 或类,比如热更新一个模块而无需重启服务。

  • 举例:
    Web 容器(如 Tomcat)的应用重新部署
    使用 URLClassLoader 加载某个模块的新版本以实现热替换

  • 为什么不能放入 classpath?
    classpath 在启动时就固定了,不能动态添加;而 URLClassLoader 可以运行时加载新 jar。

3. 多版本隔离

  • 场景说明:
    你希望不同的模块使用同一个库的不同版本,但 classpath 无法支持两个版本的同一个类。

  • 举例:
    一个服务器运行多个服务实例,它们分别依赖 log4j 的不同版本
    一个系统的插件 A 使用 fastjson 1.x,插件 B 使用 fastjson 2.x

  • 为什么不能放入 classpath?
    classpath 是共享的,会发生类冲突。使用多个 URLClassLoader,可实现类隔离。

4. 脚本或用户上传代码执行

  • 场景说明:
    系统允许用户上传 jar 或 class 文件,然后在服务端执行其中的类逻辑。

  • 举例:
    在线编程平台(如 LeetCode 后端)
    用户上传算法 jar,平台运行并返回结果

  • 为什么不能放入 classpath?
    用户上传内容是动态的,系统在运行前无法预知。

5. 实现类的延迟加载(节省资源)

  • 场景说明:
    某些类/模块体积较大或依赖较多,不希望在程序启动时就加载,只有真正使用时再加载。

  • 举例:
    大型桌面应用(如 IntelliJ)在打开某个功能模块时才加载相应 jar

总结

场景 使用 URLClassLoader 的原因
插件系统 插件动态加载,不在项目编译时可知
热部署 动态替换模块,无需重启
多版本共存 避免类冲突,实现类加载隔离
用户上传 jar 内容动态生成,classpath 无法预先配置
延迟加载模块 启动更快,节省内存
  • 如果你是在做框架设计或需要动态扩展能力的场景,理解并使用 URLClassLoader 会非常有帮助。

场景假设

调用外部jar

  • 我们有一个外部 jar 文件:hello-plugin.jar,它包含一个类:

1
2
3
4
5
6
7
8
// 这个类编译后打包进 hello-plugin.jar
package com.example.plugin;

public class HelloPlugin {
public void sayHello(String name) {
System.out.println("Hello from plugin! :" + name);
}
}
  • 主程序使用 URLClassLoader 动态加载这个 jar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class PluginLoader {
public static void main(String[] args) throws Exception {
// 外部 jar 的路径
File jarFile = new File("plugins/hello-plugin.jar");
URL jarUrl = jarFile.toURI().toURL();
// HTTPS jar 的 URL
// URL jarUrl = new URL("https://your-domain.com/libs/hello-plugin.jar");

// 构建 URLClassLoader(也可以设置父加载器)
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl});

// 加载插件类
Class<?> clazz = classLoader.loadClass("com.example.plugin.HelloPlugin");

// 创建实例并调用方法
Object plugin = clazz.getDeclaredConstructor().newInstance();
Method sayHello = clazz.getMethod("sayHello", String.class);
sayHello.invoke(plugin, "World");

// 关闭 ClassLoader(Java 7+ 推荐)
classLoader.close();
}
}

运行时动态编译 Java 源代码

  • 我们有一个 java 代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;

public class DynamicJavaRunner {
public static void main(String[] args) throws Exception {
String className = "com.example.dynamic.Hello";

// Java 源代码
String sourceCode =
"package com.example.dynamic;\n" +
"\n" +
"public class Hello {\n" +
" public void say() {\n" +
" System.out.println(\"Hello from dynamic source!\");\n" +
" }\n" +
"}\n";

// 创建临时目录
Path tempDir = Files.createTempDirectory("dynamic-classes");
File outputDir = tempDir.toFile();
// System.out.println("临时目录:" + outputDir.getAbsolutePath());

// 创建 .java 文件
File javaFile = new File(outputDir, "com/example/dynamic/Hello.java");
javaFile.getParentFile().mkdirs();
try (FileWriter writer = new FileWriter(javaFile)) {
writer.write(sourceCode);
}

// 编译 Java 文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("请用 JDK 而非 JRE 运行!");
return;
}
int result = compiler.run(null, null, null, javaFile.getPath());
if (result != 0) {
System.err.println("编译失败!");
return;
}

// 加载类并调用
URLClassLoader classLoader = new URLClassLoader(new URL[]{outputDir.toURI().toURL()});
Class<?> clazz = classLoader.loadClass(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("say");
method.invoke(instance);

// 清理(可选)
classLoader.close();
// 删除临时目录
Files.walk(outputDir.toPath())
.sorted(Comparator.reverseOrder()) // 先删文件,再删目录
.map(Path::toFile)
.forEach(File::delete);
}
}
  • 关键点说明

机制 说明
JavaCompiler JDK 自带的编译器(tools.jar
ToolProvider.getSystemJavaCompiler() 只能在 JDK 环境中工作,JRE 无法使用
URLClassLoader 用于加载编译后的 .class 文件
FileWriter 保存代码为临时 Java 文件

自定义类加载器

  • 要自定义 ClassLoader,只需要继承于 ClassLoader 或者 SecureClassLoader,并重写 findClass() 方法即可

  • 示例:我们对上面的代码进行升级,不创建临时文件,而是在内存中编译代码,并从内存中加载字节码,而不依赖磁盘文件,更高效也更适合生产环境中动态类加载(如在线代码执行、脚本引擎等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.lang.reflect.*;

public class InMemoryJavaRunner {
public static void main(String[] args) throws Exception {
String className = "com.example.dynamic.Hello";
String sourceCode =
"package com.example.dynamic;\n" +
"public class Hello {\n" +
" public void say() {\n" +
" System.out.println(\"Hello from memory compiled class!\");\n" +
" }\n" +
"}";

// 获取系统 Java 编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("请使用 JDK 运行此程序(非 JRE)");
return;
}

// 准备编译源代码:一个 JavaFileObject 表示源代码
JavaFileObject sourceFile = new JavaSourceFromString(className, sourceCode);

// 创建自定义的内存文件管理器(替代标准的磁盘管理器)
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null);
MemoryJavaFileManager fileManager = new MemoryJavaFileManager(standardFileManager);

// 执行编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Collections.singletonList(sourceFile));
boolean success = task.call();
if (!success) {
System.err.println("编译失败!");
return;
}

// 加载编译后的类
Map<String, byte[]> classBytes = fileManager.getClassBytes();
InMemoryClassLoader classLoader = new InMemoryClassLoader(classBytes);
Class<?> clazz = classLoader.loadClass(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("say");
method.invoke(instance);
}

// 用于表示内存中的 Java 源代码
static class JavaSourceFromString extends SimpleJavaFileObject {
final String code;

JavaSourceFromString(String className, String code) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

// 将编译的 class 文件保存在内存中(不是文件系统)
static class MemoryJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final Map<String, ByteArrayOutputStream> classOutputBuffers = new HashMap<>();

MemoryJavaFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
}

@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
classOutputBuffers.put(className, outputStream);
return new SimpleJavaFileObject(
URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind) {
@Override
public OutputStream openOutputStream() {
return outputStream;
}
};
}

public Map<String, byte[]> getClassBytes() {
Map<String, byte[]> result = new HashMap<>();
for (Map.Entry<String, ByteArrayOutputStream> entry : classOutputBuffers.entrySet()) {
result.put(entry.getKey(), entry.getValue().toByteArray());
}
return result;
}
}

// 自定义 ClassLoader,用于从内存中加载字节码
static class InMemoryClassLoader extends ClassLoader {
private final Map<String, byte[]> classBytes;

public InMemoryClassLoader(Map<String, byte[]> classBytes) {
super(ClassLoader.getSystemClassLoader());
this.classBytes = classBytes;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从内存中获取字节码
byte[] bytes = classBytes.get(name);
if (bytes == null) {
return super.findClass(name);
}
// 定义类并返回,一定要使用 defineClass 方法
return defineClass(name, bytes, 0, bytes.length);
}
}
}
  • 进一步升级:我们现在将这个系统扩展为支持多个类同时动态编译、内存加载并执行。这对于需要处理多个类(例如接口 + 实现、内部依赖等)非常实用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.lang.reflect.*;

// 主类
public class MultiClassInMemoryCompiler {
public static void main(String[] args) throws Exception {
// 准备多个类的源代码
Map<String, String> sources = new HashMap<>();
sources.put("com.example.api.Greeter",
"package com.example.api;\n" +
"public interface Greeter {\n" +
" void greet();\n" +
"}\n");

sources.put("com.example.impl.EnglishGreeter",
"package com.example.impl;\n" +
"import com.example.api.Greeter;\n" +
"public class EnglishGreeter implements Greeter {\n" +
" public void greet() {\n" +
" System.out.println(\"Hello from EnglishGreeter!\");\n" +
" }\n" +
"}\n");

String entryClassName = "com.example.impl.EnglishGreeter";

// 编译
Map<String, byte[]> compiledClasses = compile(sources);
if (compiledClasses == null) {
System.err.println("编译失败");
return;
}

// 加载并执行
InMemoryClassLoader classLoader = new InMemoryClassLoader(compiledClasses);
Class<?> clazz = classLoader.loadClass(entryClassName);
Object obj = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("greet");
method.invoke(obj);
}

// 编译器入口,支持多个类
public static Map<String, byte[]> compile(Map<String, String> sources) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
System.err.println("请使用 JDK 运行");
return null;
}

List<JavaFileObject> compilationUnits = new ArrayList<>();
for (Map.Entry<String, String> entry : sources.entrySet()) {
compilationUnits.add(new JavaSourceFromString(entry.getKey(), entry.getValue()));
}

StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
MemoryJavaFileManager memManager = new MemoryJavaFileManager(stdManager);

JavaCompiler.CompilationTask task = compiler.getTask(null, memManager, null, null, null, compilationUnits);
boolean success = task.call();
return success ? memManager.getClassBytes() : null;
}

// 源文件表示(Java 代码以字符串提供)
static class JavaSourceFromString extends SimpleJavaFileObject {
final String code;

JavaSourceFromString(String className, String code) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}

// 内存中的编译输出管理器
static class MemoryJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final Map<String, ByteArrayOutputStream> classOutputBuffers = new HashMap<>();

MemoryJavaFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
}

@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
classOutputBuffers.put(className, outputStream);
return new SimpleJavaFileObject(
URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind) {
@Override
public OutputStream openOutputStream() {
return outputStream;
}
};
}

public Map<String, byte[]> getClassBytes() {
Map<String, byte[]> result = new HashMap<>();
for (Map.Entry<String, ByteArrayOutputStream> entry : classOutputBuffers.entrySet()) {
result.put(entry.getKey(), entry.getValue().toByteArray());
}
return result;
}
}

// 用于加载内存中的 class
static class InMemoryClassLoader extends ClassLoader {
private final Map<String, byte[]> classBytes;

public InMemoryClassLoader(Map<String, byte[]> classBytes) {
super(ClassLoader.getSystemClassLoader());
this.classBytes = classBytes;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classBytes.get(name);
if (bytes == null) return super.findClass(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
}

打破双亲委派,实现同类多版本共存

  • 假设我们有同一个jar包的不同版本,比如:a-1.0.jara-2.0.jar,他们具有同名的 DemoClass类 ,系统classpath中引入的是 a-1.0.jar,而此时我们通过⾃定的ClassLoader加载 a-2.0.jar,并调用 DemoClass类 ,我们会发现,⾃定的ClassLoader加载的类依旧是 a-1.0.jar 中的类,而不是 a-2.0.jar 中的类。

  • 为什么会出现这种情况呢?这就是因为JDK的双亲委派机制。⾃定的ClassLoader的parent属性指向的是JDK内的AppClassLoader,⽽ AppClassLoader 会加载系统当中的所有代码,就包括 a-1.0.jar中的 DemoClass类。这时,⾃定的ClassLoader去加载 DemoClass类时,通过双亲委派向上查找,⾃然加载出来的就是APPClassloader中的DemoClass了。

  • 如何打破双亲委派呢?我们可以通过重写 loadClass()方法,来打破双亲委派,实现类⽂件的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.SecureClassLoader;

public class CustomClassLoader extends SecureClassLoader {
private URL jarUrl;

public CustomClassLoader(URL jarUrl) {
// 创建一个独立的子类加载器,只加载指定 JAR
this.jarUrl = jarUrl;
}

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 把双亲委派机制反过来,先到⼦类加载器中加载,加载不到再去⽗类加载器中加载。
synchronized (getClassLoadingLock(name)) {
// 如果类已加载,则直接返回
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
loadedClass = findClass(name);
} else {
// 委派给父类加载器
loadedClass = super.loadClass(name, resolve);
}
return loadedClass;
}
}


@Override
protected Class<?> findClass(String name) {
int code;
try {
// 访问jar包的url
URLConnection urlConnection = jarUrl.openConnection();
urlConnection.setUseCaches(false);
InputStream is = urlConnection.getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((code = is.read()) != -1) {
bos.write(code);
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
return null;
}
}
}
  • 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.File;
import java.net.URL;

public class Test {
public static void main(String[] args) throws Exception {
File jarFile = new File("path/to/a-2.0.jar");
URL jarUrl = jarFile.toURI().toURL();

CustomClassLoader loader = new CustomClassLoader(jarUrl);

// 加载 DemoClass
Class<?> clazz = loader.loadClass("com.example.DemoClass");
Object obj = clazz.getDeclaredConstructor().newInstance();
clazz.getMethod("print").invoke(obj);
}
}

打破双亲委派机制的典型场景

Tomcat类加载器

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

  • CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可⻅;

  • SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可⻅,但是对于Tomcat容器不可⻅;

  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可⻅,⽐如加载war包⾥相关的类,每个war包应⽤都有⾃⼰的WebappClassLoader,实现相互隔离,⽐如不同war包应⽤引⼊了不同的spring版本,这样实现就能加载各⾃的spring版本;

  • Jsp类加载器:针对每个JSP⻚⾯创建⼀个加载器。这个加载器⽐较轻量级,所以Tomcat还实现了热加载,也就是JSP只要修改了,就创建⼀个新的加载器,从⽽实现了JSP⻚⾯的热更新。