Learn · concepts
什么是 Java 类加载器??
类加载器在运行时在JVM中查找并加载Java类。 它们的工作方式、层级结构的重要性以及模组为何依赖于它们,请参见下文.
类加载器是 Java 运行时环境的一部分,负责查找类文件并将它们加载到 JVM 中。 每次你的代码首次访问一个类型时,JVM 都会询问一个类加载器:“帮我找到这个类。” 加载器会定位字节码,读取它,然后 JVM 在内存中定义该类。 这种机制在正常的 Java 程序中不可见,但它是模组框架用来修补 Minecraft 的确切钩子。
类加载器实际上做什么
类加载器将一个类名转换为运行中的 JVM 内部可用的类。 你请求一个类型,加载器会找到匹配的 .class 文件(或生成它),验证并链接它,然后该类就可以使用了。
工作分为三个步骤:
- 查找 — 加载器定位名为类的字节码(来自 JAR 文件、磁盘、网络或动态生成)。
- 验证和链接 — JVM 检查字节码是否有效,并解析对其他类的引用。
- 使用 — 类已准备就绪,你的程序可以实例化它。
这就是为什么 Java 程序可以在运行时动态加载在编译时未提及的代码。 类名就足以让加载器去找到它。
类加载器层次结构
类加载器以父子链的形式排列,其中大多数加载器首先询问其父加载器。 一个标准的 Java 应用程序有三个内置的类加载器:
| 加载器 | 加载内容 | 目的 |
|---|---|---|
| 启动加载器 | 核心 Java 运行时类 | 确保核心类型(如 java.lang.Object)始终相同 |
| 平台加载器 | 核心之外的标准库模块 | Java 模块和扩展 |
| 应用加载器 | 您程序自己的类 | 您程序指定的类路径 |
当应用加载器被请求加载一个类时,它通常会首先向上委托。它会询问平台加载器,平台加载器再询问启动加载器,启动加载器尝试加载它。只有当没有父加载器能够加载它时,子加载器才会尝试。这就是委托模型,它防止了相同类以不同的名称被加载两次。
类标识及其工作原理
一个类由其名称和定义它的加载器来标识。不同的加载器可以各自加载一个名为 com.example.MyClass 的类,JVM 将它们视为两种完全不同的类型。这听起来可能有些晦涩,但它是 Java 可以做的所有有趣事情的基础。
Bootstrap Loader: java.lang.Object (the core Object)
Application Loader: java.lang.Object (if somehow re-loaded by app loader)
→ Two different types, same name
→ This breaks your program, which is why delegation prevents it
自定义类加载器
程序可以定义自己的类加载器。自定义加载器可以从任何地方读取字节码:JAR 文件、网络、数据库,或它动态生成或转换的字节码。方法是:
- 插件系统在编译时未知的情况下动态加载代码。
- 应用服务器(Tomcat、JBoss)在同一JVM中运行多个应用程序,每个应用程序都有自己的类加载器。
- Minecraft模组在加载时会修补游戏类。
为什么这对Minecraft模组很重要
模组存在是因为自定义类加载。一个Minecraft模组加载器会通过自己的自定义类加载器启动游戏,以便在加载时看到和修改游戏类。拦截点就是魔法发生的地方。
当加载器位于磁盘和JVM之间时,它可以在JVM定义类之前转换该类:
Disk: obfuscated Minecraft class
↓
Mod Loader (custom classloader)
↓ [bytecode transformation happens here]
↓
JVM: defines the patched class
↓
Memory: game runs with mods active
模组可以重写游戏方法,注入新行为,添加字段,而无需触及磁盘上的游戏JAR。JVM最终运行的是模组框架修改过的类。这正是Fabric、Forge和其他模组加载器修补Minecraft的方式。
延迟加载
并非所有类都在启动时加载。JVM在第一次实际使用类时加载该类。这被称为延迟加载:
Program starts → JVM doesn't load String class yet
User calls System.out.println() → String is needed → Loader loads it
这意味着类加载器只需要找到并加载你的代码实际使用的类。未使用的类永远不会进入内存。这就是为什么你的Minecraft启动可能需要一段时间(模组添加了大量的类),但不会永远卡住的原因。
常见问题
首次使用时,而非启动时。当你的代码首次接触到某个类型时,JVM 会请求一个类加载器去查找并定义它。这被称为延迟加载,也是为什么未使用的代码永远不会进入内存的原因.
类加载器会先请求其父类加载器加载一个类,然后再尝试自己加载。父类加载器链一直向上延伸到启动加载器。这可以防止不同的加载器以相同的名称加载同一个类两次.
是的,如果不同的类加载器定义了它们。JVM 通过类名和定义它的加载器来识别一个类,因此来自不同加载器的同名类会被视为不同的类型.
拦截加载的类并在内存中转换它们。一个自定义加载器位于磁盘和 JVM 之间,在 JVM 定义类之前修改字节码。这就是模组如何修改 Minecraft,而无需修改游戏 jar 文件.