class 文件是类的二进制数据常见的存储方式,我们需要使用 ClassLoader 来将其加载到内存中,对其进行验证、解析、初始化,并在这一过程中创建java.lang.Class实例。
在典型的 Java 虚拟机 Hotspot 中,Class实例和String常量会保存在堆区(Heap),类的元数据(Metadata)会保存在元数据区(Metaspace)。
类的加载过程可分为加载、链接、初始化三个步骤。
加载是类加载过程中最重要的一步,绝大多数的 CoreMod 修改 class 文件也是在这一步进行的。
首先 ClassLoader 根据给定的类名寻找类的二进制数据,这个数据可能是磁盘上的 class 文件,也可能是通过代码生成出来的二进制数据,自定义 ClassLoader 往往覆写了寻找这一数据的方法 findClass ,以便对类进行修改或自定义 ClassPath 以外的类来源。
然后将其存储进方法区,最后创建Class实例作为方法区的访问入口。每个类的 Class 实例在 ClassLoader 中需要被妥善缓存,在尝试加载已有的类时应当返回现有的 Class 实例,这样可以保证单一 ClassLoader 中每个类最多被加载一次。
链接还可以分为验证、准备、解析三步。 首先验证文件格式、元数据、字节码,然后为静态变量分配内存以及使用各类零值给予默认值,最后将符号引用替换为直接引用。
静态变量初始化赋值和static代码块会被编译为<clinit>方法,初始化阶段会调用这个方法。
如果 ClassLoader 实现过程中通过恰当的缓存机制保障了在每个类最多被加载一次,也可以保障类的初始化也最多被执行一次。
双亲委派模型简单来说就是 ClassLoader 受到加载类的请求时,首先询问自己的父类能否进行加载,如果父类无法加载,再自己尝试加载。
在典型的 Hotspot Java 8 虚拟机中,一般分为用于加载用户类的 AppClassLoader、加载平台类的ExtClassLoader、加载系统类的BootstrapLoader,后者优先级高于前者。
不过,Java 程序加载所使用的 ClassLoader 理应对开发者是透明的,ClassLoader 的实现可能会随时发生变化,例如 Java 9 就不再使用 URLClassLoader 作为 AppClassLoader 的类型、 ExtClassLoader 替换为 PlatformClassLoader,如果我们拘泥于其具体的实现,在程序开发中难免会因此遇到错误。我们应当关注应用程序类是被多个 ClassLoader 使用双亲委派模型加载的,并在自定义 ClassLoader 妥善使用这个模型,不建议覆写 loadClass 方法,这个方法是双亲委派模型的具体实现。
双亲委派模型最大的优点是保障 ClassLoader 各司其职,极大程度的避免了类被错误、重复加载。
通过对类加载的简单介绍,我们了解了基本的类加载流程,可以在这基础上对基本的 ClassLoader 进行扩展,实现自定义 ClassLoader 。
自定义 ClassLoader 是 ModLauncher、LaunchWrapper 动态修改 Minecraft 的基础。
自定义 ClassLoader 一般有以下用途:
- 动态修改类(CoreMod常用)
- 动态加载类(例如动态解密类、加载网络类)
- 隔离加载类(例如ModLauncher将CoreMod与Minecraft隔离,防止不当类加载)
自定义 ClassLoader ,需要继承 ClassLoader 类或其子类(例如 URLClassLoader),一般会覆写以下方法中的一个或多个:
findClass,这个方法接收一个类名,需要实现寻找这个类并返回对应的Class对象,再次强调需要做好缓存,切忌重复加载类loadClass,这个方法接收一个类名,需要实现加载这个类并返回对应的Class对象,也再次强调需要实现双亲委派模式
推荐覆写 findClass 而不是 loadClass ,前者只需要完成寻找类的二进制数据,并进行 defineClass ,后者需要妥善处理双亲委派模型。