Java类加载按需触发、分阶段进行,仅在七种主动使用情形下初始化并倒逼加载链接;遵循双亲委派模型确保核心类如String不被污染;loadClass负责委派,defineClass真正转换字节码为Class对象。

Java 类的加载不是一次性完成的,而是按需触发、分阶段进行的——类只有在首次主动使用时才开始加载,且整个过程由类加载器(ClassLoader)协作完成,不满足“主动使用”条件时,即使写了 Class.forName("Xxx") 也可能跳过初始化。
类加载的触发时机:什么算“主动使用”
只有以下七种情况才会触发类的初始化(即执行 方法),进而倒逼加载、链接(验证、准备、解析):
- 创建该类的实例(
new Xxx()) - 调用该类的静态方法(
Xxx.staticMethod()) - 访问该类的静态字段(
Xxx.STATIC_FIELD),但**不包括被final修饰的编译期常量**(如public static final int VAL = 123;) - 反射调用(
Class.forName("Xxx"),注意:带布尔参数的Class.forName(name, false, loader)可跳过初始化) - 子类初始化时,若父类未初始化,则先触发父类初始化
- 启动类(main 方法所在类)被 JVM 启动时
- 使用 JDK 7+ 的动态语言支持(如
invokedynamic)且对应CallSite初始化时
常见误区:MyClass.class 字面量、数组声明 MyClass[] arr、继承关系检查、静态字段的引用但值为编译期常量——这些都不会触发初始化,自然也不强制加载。
双亲委派模型:为什么 String 不会被自定义类加载器污染
类加载器不是孤立工作的,而是按“启动类加载器 → 扩展类加载器 → 应用类加载器 → 自定义类加载器”的链式结构向上委托。核心逻辑在 ClassLoader.loadClass(String name) 中体现:
立即学习“Java免费学习笔记(深入)”;
- 先检查该类是否已加载(
findLoadedClass(name)) - 若未加载,且存在父加载器,则调用
parent.loadClass(name) - 父加载器返回
null或抛出ClassNotFoundException后,才调用本加载器的findClass(name)
这意味着 java.lang.String 永远由启动类加载器加载,即使你在应用类加载器里重写了一个同名类,JVM 也会拒绝加载——因为委托链顶端已提供合法版本。绕过双亲委派(如重写 loadClass 而不调用父类)需谨慎,容易破坏类型隔离和安全边界。
loadClass vs defineClass:谁真正把字节码变成 Class 对象
loadClass 是入口方法,负责调度与委派;真正将二进制字节流转换为 JVM 内部 Class 实例的是 defineClass:
-
loadClass("com.example.Foo"):可能从磁盘读取、网络拉取、或生成字节码,最终调用defineClass -
defineClass("com.example.Foo", byte[], offset, len):校验字节码格式、分配内存、生成运行时常量池等,返回Class>对象(此时尚未初始化) - 后续若需初始化,必须显式调用
resolveClass(Class)或等待首次主动使用
自定义类加载器时,绝不能直接覆盖 loadClass 来跳过委派,而应重写 findClass 并在其中调用 defineClass。否则会破坏双亲委派,引发 NoClassDefFoundError 或 LinkageError。
常见加载失败错误:从 ClassNotFoundException 到 NoClassDefFoundError
二者表面相似,根源完全不同:
-
ClassNotFoundException:在运行期尝试通过类名加载类时,**类加载器找不到对应的 class 文件或字节码源**(如Class.forName("Missing")失败) -
NoClassDefFoundError:类在编译期存在,但在运行期**加载或链接阶段失败后,再次引用该类时抛出**(典型场景:A 类引用 B 类,B 类静态块抛异常导致初始化失败,之后任何对 B 的访问都触发此错误) -
LinkageError(如IncompatibleClassChangeError):同一类被不同类加载器重复定义,或签名不兼容(如接口变抽象类)
排查时优先看堆栈中是 Exception 还是 Error,再结合类加载日志(加 -verbose:class)确认是否真的加载过、由哪个加载器加载、是否发生过链接失败。
类加载流程的真正复杂点不在“怎么走”,而在“什么时候走”和“被谁走”——同一个类名,不同类加载器实例加载出来就是不同的类型,哪怕字节码完全一致;而初始化时机的微妙差异,又直接影响静态字段赋值顺序和单例行为。这些细节不会报错,但会让问题藏得极深。








