ClassLoader类加载器

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 。 自定义 ClassLoader 是 ModLauncher、LaunchWrapper 动态修改 Minecraft 的基础。 自定义 ClassLoader 一般有以下用途:

  • 动态修改类(CoreMod常用)

  • 动态加载类(例如动态解密类、加载网络类)

  • 隔离加载类(例如ModLauncher将CoreMod与Minecraft隔离,防止不当类加载)

自定义 ClassLoader ,需要继承 ClassLoader 类或其子类(例如 URLClassLoader),一般会覆写以下方法中的一个或多个:

  • findClass,这个方法接收一个类名,需要实现寻找这个类并返回对应的 Class 对象,再次强调需要做好缓存,切忌重复加载类

  • loadClass,这个方法接收一个类名,需要实现加载这个类并返回对应的 Class 对象,也再次强调需要实现双亲委派模式

推荐覆写 findClass 而不是 loadClass ,前者只需要完成寻找类的二进制数据,并进行 defineClass ,后者需要妥善处理双亲委派模型。

Last updated