类加载发生在首次主动使用时,如访问非final静态字段、调用静态方法、class.forname()、初始化子类等;编译期常量引用、myclass.class字面量不触发初始化。

类加载到底发生在哪一刻?
类加载不是编译时的事,也不是你写完 new MyClass() 就立刻触发——它只在「首次主动使用」时才真正开始。比如访问静态字段(非 final 常量)、调用静态方法、反射 Class.forName("X")、初始化子类(且父类未初始化)、或者 JVM 启动时指定的主类。
容易踩的坑:
-
final static int VAL = 123这种编译期常量,引用它不会触发类加载——字节码里直接替换成123,连符号引用都没进常量池 -
MyClass.class字面量(如MyClass.class.getName())也不会触发初始化,只走加载+链接,跳过<clinit></clinit>执行 - 子类引用父类的静态字段,若该字段在父类中被声明为
static final且是编译期常量,则同样不触发父类初始化
加载 → 链接 → 初始化,每步干了什么?
这三步不是线性排队,而是交错推进,但关键行为边界必须清楚:
-
加载(Loading):找
.class文件(可从文件系统、JAR、网络、甚至动态生成),读成字节流,在方法区存结构,堆里建一个java.lang.Class对象——这是你能操作类元信息的唯一入口 -
链接(Linking) 分三子步:
—Verification校验字节码安全(如魔数是否CAFEBABE、指令是否越界),可用-Xverify:none关闭(仅限可信环境);
—Preparation给static变量分配内存并设零值(int→0,Object→null),但static final字面量会直接赋值;
—Resolution把常量池里的符号引用(如"java/lang/String")转成内存地址等直接引用,部分可延迟到首次使用时再做(支持多态) -
初始化(Initialization):执行
<clinit></clinit>方法——它由编译器自动生成,合并所有static赋值和静态代码块,按源码顺序执行;父类<clinit></clinit>一定先于子类执行
为什么 ClassLoader 不能随便 new?
类加载器不是工具类,它是类的“命名空间”锚点。同一个 .class 字节流,用两个不同的 ClassLoader 实例加载,得到的是两个完全无关的 Class 对象——哪怕全限定名一模一样,它们的实例也不能互相转型,instanceof 会失败,ClassLoader.getSystemClassLoader().loadClass("X") 和你自定义的 new MyLoader().loadClass("X") 加载出来的 X.class 是不同类。
常见错误现象:
- Web 应用热部署失败,报
ClassCastException:旧类和新类由不同加载器加载,看似同类型实为异类 - OSGi 或模块化场景下,服务注册/查找失败:接口类由框架加载器加载,实现类由插件加载器加载,两者不在同一命名空间
- 自定义加载器没重写
findClass()而直接重写loadClass(),破坏双亲委派,导致java.lang.String等核心类被重复加载或找不到
准备阶段设的“零值”,真能当默认值用吗?
不能。准备阶段设的只是 JVM 规定的初始零值(0/false/null),它和你在代码里写的 static int x = 5 完全是两回事——后者实际发生在初始化阶段,通过 <clinit></clinit> 中的 putstatic 指令完成。
这意味着:
- 如果某个
static字段依赖另一个尚未初始化的类(比如static Date now = new Date()),而该类又在初始化时触发循环依赖,就会卡在准备完、初始化前,抛ExceptionInInitializerError - 调试时看到某
static字段还是null,别急着怀疑代码逻辑——可能它根本还没走到初始化那步,只是停留在准备阶段 - JDK 9+ 模块系统中,模块声明里的
requires不影响准备阶段行为,但会影响解析阶段能否成功找到依赖类
类加载机制的复杂性不在步骤多,而在每个阶段的触发时机和副作用都隐含在字节码语义与运行时上下文里——稍不注意,就变成线上查不出来源的 NoClassDefFoundError 或诡异的静态字段空指针。







