类加载将二进制表示转换为 Class 对象。链接和初始化是独立步骤,
因此加载类型并不意味着其静态初始化器已经运行。
加载器和类型身份 #
JDK 提供引导、平台和应用程序类加载器。引导加载器定义核心平台类,
在部分 API 中用 null 表示;平台加载器定义平台模块;应用程序加载器从类路径或模块路径
加载应用程序类。自定义加载器通常使用双亲优先委派:
- 重用已经加载的定义;
- 请求父加载器加载二进制名称;然后
- 查找字节并调用
defineClass。
双亲优先委派保护平台类型身份。它是一种约定而不是绝对规则:
插件系统有时会在严格排除特定软件包的情况下使用子优先加载。
运行时类型由其二进制名称和定义它的加载器共同标识。
因此,两个加载器可以定义不同的 com.example.Plugin 类型,实例也不能在两者之间强制转换。
加载、链接和初始化 #
JVM 规范将生命周期分为以下阶段:
- 加载获取字节,创建
Class对象,并关联定义加载器。 - 链接包括验证、准备和可选解析。验证检查结构和类型安全约束。 准备为静态字段分配空间并赋予默认值。解析将符号引用转换为运行时引用; 实现可以推迟解析。
- 初始化执行合成的
<clinit>方法,其中包含静态字段初始化器和按源代码顺序排列的静态块。
编译期常量在准备阶段赋值,因此读取常量变量可能避免初始化其声明类。 不要根据某个实现的日志输出推断可移植的解析调度。
精确的初始化触发条件 #
JLS §12.4.1 要求在首次主动使用类之前立即初始化类:
创建实例、调用静态方法、赋值静态字段,或读取不是常量变量的静态字段。
主动使用还包括执行需要该类的 static 断言、选定的反射操作以及规范规定的方法句柄行为。
Class.forName 默认会初始化;使用三参数重载并传入 false,可以加载但不请求初始化。
类 C 初始化前,其超类会先初始化;随后在适用时,按照 JLS 定义的顺序初始化声明默认方法的超接口。
初始化一个类不会初始化它实现的任意接口。读取 Child.value 会初始化实际声明 value 的类,
不一定是 Child。
失败和并发 #
JVM 会为每个类或接口串行化初始化。一个线程运行 <clinit> 时,其他线程会等待;
同一线程发起的递归请求则被允许。如果初始化异常结束,VM 会将该类型记录为错误,
向触发使用报告初始化失败,之后的主动使用会以 NoClassDefFoundError 失败。
必须重启进程或使用独立加载器,才能获得另一次初始化尝试。
静态初始化器应建立简单且不会失败的不变量。网络调用、获取锁和解析配置, 可能造成死锁,或在一次瞬时失败后永久污染类型。
评论
在 GitHub 上阅读或评论(发表评论需要登录)