类加载五阶段非线性执行:加载、验证、准备、解析、初始化可交叉或延迟;解析常懒执行,初始化严格按需触发;主动使用才初始化,final静态常量编译期内联;双亲委派可打破但需防linkageerror。

类加载的五个阶段不是线性执行的
Java虚拟机规范规定了加载、验证、准备、解析、初始化这五个阶段,但实际中它们可能交叉或延迟。比如解析阶段在某些情况下会推迟到首次使用该符号引用时才发生(即“懒解析”),而初始化更是严格按需触发——只有当主动使用一个类时才会真正执行其<clinit></clinit>方法。
常见错误现象:ClassNotFoundException发生在加载阶段,NoClassDefFoundError则多出现在初始化失败后(比如静态块抛异常),二者常被混淆。
- 主动使用包括:创建实例、读写静态字段(非final)、调用静态方法、反射调用、初始化子类(会先触发父类初始化)
- final修饰的静态常量(编译期可确定值)会在编译时直接内联进调用方字节码,跳过本类的
初始化阶段 - 接口的
初始化不会触发其父接口初始化,除非该接口被主动使用
双亲委派模型不是强制协议,而是推荐机制
类加载器默认遵循双亲委派:先委托父加载器尝试加载,父无法加载再自己加载。但这个逻辑完全由ClassLoader.loadClass()方法实现,你可以重写它绕过委派——比如OSGi、Tomcat的WebAppClassLoader都这么干。
使用场景:自定义类加载器用于热部署、模块隔离、加密类加载等。一旦破坏委派,要注意避免LinkageError(如两个不同加载器加载了同一类的不同版本)。
立即学习“Java免费学习笔记(深入)”;
- 不要在自定义
loadClass()里直接调用findClass(),应先调用super.loadClass()保持委派链 - 若必须打破委派(如加载WEB-INF/classes下的类),应在委派失败后再调用自己的
findClass() - 启动类加载器(Bootstrap)无法被Java代码直接引用,
ClassLoader.getSystemClassLoader().getParent()返回的是ExtClassLoader,再往上为null
准备阶段只给静态变量赋零值,不是初始值
很多人误以为public static int x = 123;在准备阶段就会被设为123。实际上,此时x被赋的是0(int类型的零值);真正的123是在初始化阶段,由<clinit></clinit>方法执行赋值指令完成的。
性能影响:这个分离设计让JVM可以在类加载早期就分配好静态变量内存空间,无需等待常量池解析完毕,提升加载效率。
- final修饰的静态基本类型或字符串常量(如
public static final int Y = 456;)是例外:它们在准备阶段就被赋予编译期确定的值 - 数组类型静态字段(如
public static int[] arr = new int[5];)在准备阶段仅分配内存并置零,对象实例化发生在初始化阶段 - 注意:
static {}块中的代码全部属于初始化阶段,不在准备阶段执行
解析阶段失败通常表现为IncompatibleClassChangeError及其子类
解析是把常量池内的符号引用转为直接引用的过程。如果目标类结构已变(比如方法签名不匹配、字段被删、继承关系被破坏),而调用方字节码未重新编译,运行时就会在解析时抛出IncompatibleClassChangeError或其子类(如NoSuchMethodError、IllegalAccessError)。
兼容性影响:这类错误往往在升级依赖库后突然出现,且堆栈不指向你自己的代码,排查困难。
- 典型诱因:A.jar里调用了B.jar中的
public void foo(String),但B.jar升级后改为public void foo(String, int),而A.jar未重编译 - JDK 9+ 模块系统强化了封装,跨模块访问非导出包内的类/方法,也会在解析时报
IllegalAccessError - 使用
javap -v查看字节码的常量池,确认符号引用是否与当前类路径下实际类结构一致
解析和初始化的延迟性——它们不一定在Class.forName()返回时完成,而可能卡在第一次调用某个方法或字段时才爆发问题。










