类加载机制
类加载机制
JVM 需要把类的字节流转换为运行时数据结构,并创建对应的 Class 对象。类的生命周期通常包括加载、连接、初始化、使用和卸载。
加载
加载阶段完成三件事:
- 根据全限定名获取二进制字节流。
- 转换为 JVM 的运行时数据结构。
- 创建代表该类型的 Class 对象。
字节流可以来自 JAR、目录、网络或动态生成。同一个类由“全限定名 + 定义它的类加载器”共同确定;名称相同但加载器不同,仍是不同类型。
连接
验证
检查字节码格式、元数据、类型和符号引用是否合法。
准备
为静态字段分配内存并设置类型默认值。例如 static int count = 10 通常先得到 0,赋值 10 在初始化阶段完成。编译期常量可能直接得到常量值。
解析
把常量池中的符号引用替换为直接引用,也可能延迟到实际使用时完成。
初始化
执行编译器生成的类初始化方法,内容来自静态字段赋值和静态初始化块,并按源码顺序组织。
常见主动初始化场景:
- new 创建对象。
- 读写非编译期常量的静态字段。
- 调用静态方法。
- 通过反射使用类。
- 初始化子类前初始化父类。
- JVM 启动时初始化主类。
访问父类声明的静态字段、创建某类型数组、引用已内联的编译期常量,不一定触发目标类初始化。
类加载器层次
| 加载器 | 主要职责 |
|---|---|
| Bootstrap ClassLoader | 加载 java.base 等核心模块,Java 层常显示为 null |
| Platform ClassLoader | 加载平台模块 |
| Application ClassLoader | 加载应用类路径或模块路径中的类 |
| 自定义 ClassLoader | 插件隔离、热部署、加密类加载等 |
双亲委派
加载请求通常先委托父加载器,父加载器无法完成时,子加载器才自行尝试。
好处:
- 避免核心类重复定义。
- 防止应用伪造 java.lang.String 等核心类。
- 让基础类型在不同加载器之间保持统一。
双亲委派是常见实现约定,不是不可改变的语法规则。Tomcat、OSGi、SPI 等场景会使用子优先或线程上下文类加载器。
loadClass 与 Class.forName
- ClassLoader.loadClass() 默认只加载,不主动初始化。
- Class.forName(name) 默认加载并初始化。
- Class.forName(name, false, loader) 可以不初始化。
常见异常
- ClassNotFoundException:显式加载时找不到类。
- NoClassDefFoundError:运行时无法定义类,或类初始化曾失败。
- ClassCastException:同名类由不同加载器定义也可能无法转换。
- ExceptionInInitializerError:静态初始化逻辑抛出异常。
排查时应同时检查类名、JAR 版本、类路径、实际加载器和最初的初始化异常。