Java 类加载

最后更新: 2023-02-25 22:30:58 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

类加载将二进制表示转换为 Class 对象。链接和初始化是独立步骤, 因此加载类型并不意味着其静态初始化器已经运行。

加载器和类型身份 #

JDK 提供引导、平台和应用程序类加载器。引导加载器定义核心平台类, 在部分 API 中用 null 表示;平台加载器定义平台模块;应用程序加载器从类路径或模块路径 加载应用程序类。自定义加载器通常使用双亲优先委派:

  1. 重用已经加载的定义;
  2. 请求父加载器加载二进制名称;然后
  3. 查找字节并调用 defineClass。

双亲优先委派保护平台类型身份。它是一种约定而不是绝对规则: 插件系统有时会在严格排除特定软件包的情况下使用子优先加载。 运行时类型由其二进制名称和定义它的加载器共同标识。 因此,两个加载器可以定义不同的 com.example.Plugin 类型,实例也不能在两者之间强制转换。

加载、链接和初始化 #

JVM 规范将生命周期分为以下阶段:

  1. 加载获取字节,创建 Class 对象,并关联定义加载器。
  2. 链接包括验证、准备和可选解析。验证检查结构和类型安全约束。 准备为静态字段分配空间并赋予默认值。解析将符号引用转换为运行时引用; 实现可以推迟解析。
  3. 初始化执行合成的 <clinit> 方法,其中包含静态字段初始化器和按源代码顺序排列的静态块。

编译期常量在准备阶段赋值,因此读取常量变量可能避免初始化其声明类。 不要根据某个实现的日志输出推断可移植的解析调度。

精确的初始化触发条件 #

JLS §12.4.1 要求在首次主动使用类之前立即初始化类: 创建实例、调用静态方法、赋值静态字段,或读取不是常量变量的静态字段。 主动使用还包括执行需要该类的 static 断言、选定的反射操作以及规范规定的方法句柄行为。 Class.forName 默认会初始化;使用三参数重载并传入 false,可以加载但不请求初始化。

类 C 初始化前,其超类会先初始化;随后在适用时,按照 JLS 定义的顺序初始化声明默认方法的超接口。 初始化一个类不会初始化它实现的任意接口。读取 Child.value 会初始化实际声明 value 的类, 不一定是 Child。

失败和并发 #

JVM 会为每个类或接口串行化初始化。一个线程运行 <clinit> 时,其他线程会等待; 同一线程发起的递归请求则被允许。如果初始化异常结束,VM 会将该类型记录为错误, 向触发使用报告初始化失败,之后的主动使用会以 NoClassDefFoundError 失败。 必须重启进程或使用独立加载器,才能获得另一次初始化尝试。

静态初始化器应建立简单且不会失败的不变量。网络调用、获取锁和解析配置, 可能造成死锁,或在一次瞬时失败后永久污染类型。

参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)