JVM 类加载与字节码概览
类如何被加载、链接与初始化,以及字节码作为执行中间表示的边界(规范 vs 实现)。
#concept
#growing
#dev
#java
[!info] related notes
JVM 类加载与字节码概览
这个概念为什么出现
ClassNotFoundException、NoClassDefFoundError、版本不匹配、SPI 与热加载问题,最终都落到类如何进入 JVM。字节码是编译产物的可验证中间表示。
[!abstract] 一句话理解 类加载大致经历加载→链接(验证/准备/解析)→初始化;字节码由类文件承载,JVM 解释/JIT 执行;不同加载器形成命名空间。
最小可观察示例
场景:确认应用类由哪个加载器装入
public class LoaderPeek {
public static void main(String[] args) {
ClassLoader cl = LoaderPeek.class.getClassLoader();
System.out.println(cl);
System.out.println(String.class.getClassLoader()); // 引导加载器,在本示例使用的 HotSpot 中表示为 null
}
}
javac LoaderPeek.java && java LoaderPeek
在普通 HotSpot 命令行运行中:第一行是应用类加载器描述(具体名称和标识不固定),第二行是 null。null 表示引导加载器,不是平台加载器。
结合场景再看三个关注点
- 非数组类身份 = 二进制名称 + 定义它的加载器;发起加载请求的加载器可能只是委托方。
- 初始化触发点(首次主动使用)影响 static 副作用。
- 字节码版本与运行 JDK 要兼容。
核心概念与准确模型
1. 加载器层级(经典模型,模块时代有演进)
- 引导 / 平台 / 应用
ClassLoader.loadClass默认先委托父加载器;自定义加载器可以改变委托策略。核心类还受到平台的加载限制,不能把默认委托当成所有加载器的强制规则。
2. 链接与初始化
- 验证字节码安全
- 准备阶段为静态字段分配存储并设置默认值;常量字段另有规范规定,普通字段初始化器与 static 块在初始化阶段执行。
- 解析可延后到符号引用首次使用,不能把整条加载链路理解为必然一次完成。
- 初始化执行类初始化逻辑(若存在则为
<clinit>);读取编译期常量、获取类字面量或创建该类的数组本身,不触发该类初始化。
3. 字节码
javap -c观察- 操作数栈与局部变量表模型
边界情况与反直觉行为
- 显式按名加载失败可抛
ClassNotFoundException;运行时需要的类无法定义/加载,或类先前初始化失败后的再次使用,可导致NoClassDefFoundError。非 Error 异常导致初始化失败时,首次通常表现为ExceptionInInitializerError。 - 循环初始化可能看到默认值。
- 容器/插件多加载器极易“同名类转换失败”。
常见误区
[!warning] 常见误区:以为类只加载一次全局唯一 不同加载器可加载同名类为不同运行时类。
工程实践
- 依赖冲突用 jdeps/构建 tree 先查。
- 框架 SPI 注意
ServiceLoader与 TCCL。 - 生产开启类加载相关诊断要克制开销。
- 读 JVMS 章节时区分必须 vs 实现。
本节总结
- 加载链路解释大量“找不到类”
- 加载器命名空间
- 字节码是可验证 IR
自测题
- 为什么容器里 ClassCastException 可能“看起来类型一样”?
- static 初始化何时跑?
参考答案
- 同一二进制名称由不同加载器分别定义,运行时类型不同;仅由不同加载器发起请求但最终委托给同一定义加载器则不会。
- 类型首次主动使用触发初始化(规范定义的触发条件)。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| JVMS Class Loading | 规范 | 加载与链接 |
| javap tool | 工具 | 反汇编 |