类加载机制
1. 这是什么
类加载机制描述了 Java 类从字节码文件进入 JVM,到可以被程序使用的全过程。
它是理解启动流程、插件机制、隔离加载和框架扩展的重要基础。
一句话理解:
- 源码编译成
.class只是第一步 - 只有类被 JVM 正确加载、链接、初始化后,程序才能真正使用它
2. 为什么重要
如果不理解类加载,很多问题会显得非常神秘,例如:
- 重复加载
- 类冲突
ClassNotFoundExceptionNoClassDefFoundError- 静态代码块为什么突然执行
理解类加载之后,这些问题会清晰很多。
3. 先建立直觉:类加载和类初始化不是一回事
这是本章最重要的第一层理解。
很多人会把下面这些动作混成一件事:
- 类被找到
- 类被加载进 JVM
- 类元数据准备好
- 静态代码块执行
- 静态变量赋值
但实际上:
- 类加载不等于类初始化
- 你拿到
SomeClass.class时,不一定已经执行了静态初始化逻辑
4. 核心内容
4.1 类从字节码到可用,大致经历哪些阶段
最常见的学习顺序是:
- 加载(Loading)
- 链接(Linking)
- 初始化(Initialization)
而“链接”又可继续分成:
- 验证
- 准备
- 解析
所以常见完整路径是:
- 加载
- 验证
- 准备
- 解析
- 初始化
4.2 每个阶段可以怎么理解
加载
把类的字节码读进来,并生成对应的 Class 对象。
验证
确认字节码是否合法、安全、符合 JVM 规范。
准备
为类变量分配内存,并设置初始默认值。
解析
把符号引用转换成直接引用。
初始化
真正执行类构造逻辑,例如:
- 静态变量显式赋值
- 静态代码块
这一步最容易被直观感知,因为你经常会看到打印输出或副作用。
4.3 哪些操作会触发类初始化
常见会触发类初始化的动作包括:
new一个类- 访问类的静态变量(非编译期常量)
- 调用类的静态方法
Class.forName(...)默认初始化
而下面这些动作不一定触发初始化:
- 使用类字面量
SomeClass.class - 访问编译期常量
4.4 双亲委派模型是什么
双亲委派模型的核心思路是:
- 类加载请求先向上委托给父加载器
- 父加载器无法完成时,子加载器再自己尝试
你可以把它理解成:
- “先问上级能不能处理,处理不了我再自己来”
4.5 双亲委派为什么重要
它的价值主要体现在:
- 保证核心类库的一致性
- 避免重复加载基础类
- 提升安全性
例如:
- 不能轻易自己写一个假的
java.lang.String来替代系统类
4.6 常见类加载器层次
常见可以这样理解:
- Bootstrap ClassLoader:加载核心类库
- Platform ClassLoader:加载平台相关类库
- AppClassLoader:加载应用类路径中的类
日常最常接触到的是:
- AppClassLoader
4.7 自定义类加载器通常用来做什么
常见场景包括:
- 插件化加载
- 热部署
- 隔离不同模块依赖
- 自定义字节码来源
但它不是“平时业务开发一定要写”的常规工具。
学习阶段更重要的是理解:
- 类加载器不同,加载出来的类在 JVM 看来就可能不是“同一个类”
4.8 ClassNotFoundException 和 NoClassDefFoundError 的区别
这两个很容易混淆。
ClassNotFoundException
通常出现在:
- 你主动按名字去加载类,但 JVM 找不到它
例如:
Class.forName("com.example.MissingClass")NoClassDefFoundError
通常出现在:
- 类在编译时是存在的
- 但运行时无法完成定义或初始化
- 或者之前初始化失败,后续再次使用时出错
它们都和“类没正常可用”有关,但触发语义不同。
5. 学习重点
这一章真正要掌握的是:
- 类加载和类初始化不是同一件事
- 双亲委派解决的是安全和一致性问题
- 类加载器不同,类身份也可能不同
ClassNotFoundException和NoClassDefFoundError语义不同- 初始化触发时机是排障和框架理解的关键
6. 常见问题
6.1 混淆 ClassNotFoundException 和 NoClassDefFoundError
一个偏“主动加载找不到”,一个偏“定义或初始化过程出了问题”。
6.2 只记双亲委派流程,不理解设计意义
如果不知道它为什么存在,就很难真正理解类隔离、安全和核心类库稳定性。
6.3 忽视多类加载器环境下的隔离问题
在插件、容器、应用服务器场景里,这个问题会非常真实。
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 ClassLoadingDemo.java,内容如下:
public class ClassLoadingDemo {
static class InitTarget {
static int value;
static {
value = 123;
System.out.println("InitTargetInitialized=true");
}
}
static class BrokenInit {
static int value = create();
private static int create() {
throw new RuntimeException("init failed");
}
}
public static void main(String[] args) throws Exception {
System.out.println("appClassLoader=" + ClassLoadingDemo.class.getClassLoader().getClass().getSimpleName());
System.out.println("platformClassLoader="
+ ClassLoader.getPlatformClassLoader().getClass().getSimpleName());
Class<?> literal = InitTarget.class;
System.out.println("literalLoaded=" + (literal != null));
System.out.println("beforeForNameNoManualOutputYet=true");
Class<?> byName = Class.forName("ClassLoadingDemo$InitTarget");
System.out.println("forNameLoaded=" + (byName != null));
System.out.println("staticValue=" + InitTarget.value);
try {
Class.forName("missing.NotExisting");
} catch (ClassNotFoundException e) {
System.out.println("classNotFoundCaptured=true");
}
try {
System.out.println(BrokenInit.value);
} catch (Throwable e) {
System.out.println("firstBrokenInit=" + e.getClass().getSimpleName());
}
try {
System.out.println(BrokenInit.value);
} catch (Throwable e) {
System.out.println("secondBrokenInit=" + e.getClass().getSimpleName());
}
}
}7.2 编译并运行
javac ClassLoadingDemo.java
java ClassLoadingDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
appClassLoader=AppClassLoader
platformClassLoader=PlatformClassLoader
literalLoaded=true
beforeForNameNoManualOutputYet=true
InitTargetInitialized=true
forNameLoaded=true
staticValue=123
classNotFoundCaptured=true
firstBrokenInit=ExceptionInInitializerError
secondBrokenInit=NoClassDefFoundError7.4 每一行在验证什么
literalLoaded=true且初始化输出还没出现:说明拿类字面量不等于一定触发初始化InitTargetInitialized=true:说明Class.forName默认会触发初始化staticValue=123:说明初始化后静态变量已按逻辑赋值classNotFoundCaptured=true:说明按名字主动加载缺失类会触发ClassNotFoundExceptionfirstBrokenInit=ExceptionInInitializerError:说明类初始化失败时,第一次通常体现为初始化异常secondBrokenInit=NoClassDefFoundError:说明初始化失败后再次使用该类,会进入定义不可用状态
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 把
Class.forName("ClassLoadingDemo$InitTarget")改成只访问InitTarget.class - 给
InitTarget增加一个static final int CONST = 1;,然后只读取这个编译期常量
你可以观察:
- 类加载和初始化是可以拆开的
- 编译期常量的访问语义和普通静态字段不同
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 自己画一张类加载阶段图
- 验证哪些操作会触发类初始化
- 区分总结
ClassNotFoundException和NoClassDefFoundError - 阅读一个使用自定义类加载器的插件示例
9. 自测问题
- 双亲委派模型为什么重要?
- 类加载和类初始化有什么区别?
Class.forName和SomeClass.class的行为差别是什么?ClassNotFoundException和NoClassDefFoundError的触发语义有什么区别?- 自定义类加载器通常用于哪些场景?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 类加载包含加载、链接、初始化等阶段
- 初始化是静态逻辑真正执行的关键步骤
- 双亲委派保证核心类库的一致性与安全性
- 类加载器不同可能导致同名类身份不同
ClassNotFoundException和NoClassDefFoundError不应混为一谈