类加载、初始化与类加载器
学习目标
- 区分加载、验证、准备、解析和初始化五个阶段。
- 判断哪些主动使用会触发
<clinit>,以及初始化失败如何传播。 - 理解双亲委派、类加载器身份和插件隔离的边界。
核心知识点
专业术语
- 加载(loading):把类的二进制表示读入 JVM,并创建对应 Class 对象。
- 链接(linking):包含验证、准备和解析,让类满足执行条件。
- 初始化(initialization):执行静态字段显式赋值和静态初始化块,逻辑上对应
<clinit>。 - 类加载器(ClassLoader):负责按名称查找和定义类的对象。
- 双亲委派(parent delegation):加载请求先交给父加载器,失败后子加载器再尝试。
白话解释与边界
加载是“把字节读进来”,链接是“检查并准备好”,初始化才是“执行静态代码”。Type.class、数组类型和常量变量的使用不一定初始化类;主动调用静态方法、读取非常量静态字段、反射初始化等才会触发初始化。
类的身份由二进制名称和定义它的 ClassLoader 共同决定。两个插件各自加载同名类,可以隔离依赖,但不能把一个加载器定义的对象直接强转成另一个加载器定义的同名类型。
常用用法
Class.forName:按名称加载并初始化
默认的 Class.forName(String) 会使用调用者的初始化语义,类不存在时抛 ClassNotFoundException。
// 说明:Class.forName:按名称加载并初始化。
public class LoadAndInitializeDemo {
static class Feature {
static {
System.out.println("feature initialized");
// 输出:feature initialized
}
}
public static void main(String[] args) throws Exception {
Class.forName(Feature.class.getName());
// 初始状态:Class.forName(Feature.class.getName())。
// 作用:Class.forName(Feature.class.getName());;按名称加载并初始化。
}
}动态加载插件时应明确初始化时机,避免探测类就执行副作用。
Class.forName(name, false, loader):只加载不初始化
这个重载适合框架先验证类存在、再在真正使用时初始化。
// 说明:Class.forName(name, false, loader):只加载不初始化。
public class LoadWithoutInitializeDemo {
static class Plugin {
static {
System.out.println("should not print yet");
// 输出:should not print yet(主动初始化 Plugin 时)
}
}
public static void main(String[] args) throws Exception {
Class.forName(Plugin.class.getName(), false, Plugin.class.getClassLoader());
// 初始状态:Class.forName(Plugin.class.getName(), false, Plugin.class.getClassLoader())。
// 作用:Class.forName(Plugin.class.getName(), false, Plugin.class.getClassLoader());;只加载不初始化。
System.out.println("loaded without init");
// 输出:loaded without init
}
}链接可能发生,但静态初始化块不会在 false 这一步执行。
ClassLoader.getParent:查看委派链
应用类加载器通常有平台类加载器作为父级,平台类加载器再由 bootstrap loader 支持;getParent() 返回 null 不代表没有更高层逻辑,只是 bootstrap loader 没有 Java 对象实例。
// 说明:ClassLoader.getParent:查看委派链。
public class ClassLoaderParentDemo {
public static void main(String[] args) {
ClassLoader app = ClassLoader.getSystemClassLoader();
// 作用:ClassLoader app = ClassLoader.getSystemClassLoader();;查看委派链,返回读取结果。
// 初始状态:app = ClassLoader.getSystemClassLoader()。
ClassLoader parent = app.getParent();
// 初始状态:parent = app.getParent()。
// 作用:ClassLoader parent = app.getParent();;查看委派链,返回读取结果。
System.out.println(app != null);
// 输出:true
System.out.println(parent != null);
// 输出:true(常见 JDK 配置;具体环境可能不同)
}
}Class.getClassLoader:确认类型由谁定义
定义加载器用于判断类型身份、插件隔离和类冲突。
// 说明:Class.getClassLoader:确认类型由谁定义。
public class DefiningLoaderDemo {
public static void main(String[] args) {
ClassLoader loader = DefiningLoaderDemo.class.getClassLoader();
// 初始状态:loader = DefiningLoaderDemo.class.getClassLoader()。
// 作用:ClassLoader loader = DefiningLoaderDemo.class.getClassLoader();;确认类型由谁定义。
System.out.println(loader == ClassLoader.getSystemClassLoader());
// 输出:true
}
}类路径相同也不代表 Class 对象相同,诊断 ClassCastException 时要打印类型名和 classLoader。
验证静态初始化顺序:观察父类、字段与代码块
仅取得 Child.class 不会触发初始化,因此上例不会打印 base/child;改为 new Child() 或主动访问 Child 的非编译期常量静态成员时,会先初始化父类再初始化子类。
// 说明:验证静态初始化顺序:观察父类、字段与代码块。
public class InitializationOrderDemo {
static class Base {
static {
System.out.println("base");
// 输出:base(主动初始化 Child 时)
}
}
static class Child extends Base {
static {
System.out.println("child");
// 输出:child(主动初始化 Child 时)
}
}
public static void main(String[] args) {
Class<?> type = Child.class;
// 作用:Class<?> type = Child.class;;观察父类、字段与代码块。
// 初始状态:type = Child.class。
System.out.println(type.getSimpleName());
// 输出:Child
}
}ServiceLoader:按接口发现实现类
ServiceLoader 通过模块或 META-INF/services 配置发现实现,实际加载和初始化由对应 ClassLoader 完成。
// 说明:ServiceLoader:按接口发现实现类。
import java.util.ServiceLoader;
public class ServiceLoaderDemo {
interface Formatter {
String format(String value);
}
public static void main(String[] args) {
long count = ServiceLoader.load(Formatter.class).stream().count();
// 初始状态:count = ServiceLoader.load(Formatter.class).stream().count()。
// 作用:long count = ServiceLoader.load(Formatter.class).stream().count();;按接口发现实现类。
System.out.println("providers=" + count);
// 输出:providers=0(没有配置服务提供者时)
}
}服务配置缺失、实现类不可见或依赖冲突都要结合加载器诊断。
不常用但需要知道
自定义 ClassLoader:重写 findClass
生产自定义加载器通常在 findClass 中读取字节并调用 defineClass;重写 loadClass 时要谨慎保留父委派,否则可能重复定义核心接口或造成类型隔离。关闭插件时还要断开线程、ThreadLocal、静态缓存和 ClassLoader 引用,类才有机会卸载。
public class CustomLoaderDemo {
static class PluginLoader extends ClassLoader {
PluginLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
// 初始状态:throw new ClassNotFoundException(name)。
// 作用:throw new ClassNotFoundException(name);;重写 findClass,返回读取结果。
}
}
public static void main(String[] args) {
ClassLoader loader = new PluginLoader(CustomLoaderDemo.class.getClassLoader());
// 初始状态:loader = new PluginLoader(CustomLoaderDemo.class.getClassLoader())。
// 作用:ClassLoader loader = new PluginLoader(CustomLoaderDemo.class.getClassLoader());;重写 findClass,返回读取结果。
System.out.println(loader.getParent() != null);
// 输出:true
}
}ClassValue:按 Class 关联可回收元数据
ClassValue 适合给第三方类型关联缓存元数据,并随 Class 卸载回收;普通静态 Map 可能反过来强引用 ClassLoader,造成插件无法卸载。
// 说明:NAMES.get(String.class) 首次调用 computeValue 并把 "String" 关联到该 Class;Class 可卸载时这份元数据不会被静态 Map 键强留。
public class ClassValueDemo {
private static final ClassValue<String> NAMES = new ClassValue<>() {
// 初始状态:NAMES = new ClassValue<>()。
// 作用:private static final ClassValue<String> NAMES = new ClassValue<>() {;按 Class 关联可回收元数据。
@Override
protected String computeValue(Class<?> type) {
return type.getSimpleName();
// 作用:return type.getSimpleName();;按 Class 关联可回收元数据,返回读取结果。
}
};
public static void main(String[] args) {
System.out.println(NAMES.get(String.class));
// 输出:String
}
}initialize 失败:ExceptionInInitializerError
初始化异常会标记该类初始化失败,后续主动使用可能得到 NoClassDefFoundError。静态初始化应保持轻量并验证配置,复杂启动动作交给显式生命周期方法。
// 说明:Class.forName 主动初始化 Broken,静态块的 "bad config" 异常被 JVM 包装为 ExceptionInInitializerError。
public class InitializationFailureDemo {
static class Broken {
static { if (true) throw new IllegalStateException("bad config"); }
// 作用:static { if (true) throw new IllegalStateException("bad config"); };ExceptionInInitializerError。
}
public static void main(String[] args) {
try {
Class.forName(Broken.class.getName());
// 初始状态:Class.forName(Broken.class.getName())。
// 作用:Class.forName(Broken.class.getName());;ExceptionInInitializerError。
} catch (ExceptionInInitializerError ex) {
System.out.println(ex.getClass().getSimpleName());
// 输出:ExceptionInInitializerError
}
}
}预加载类:避免把加载时机误当初始化保证
JVM 可以提前加载、解析或编译,但语言层面只保证需要的初始化语义。性能问题要用类加载日志、JFR 和启动基线验证,不能根据“类一定在启动时加载”推理。
public class LoadingTimingDemo {
public static void main(String[] args) {
System.out.println("loading timing is implementation dependent");
// 输出:loading timing is implementation dependent
}
}简单案例
public class ClassIdentityDemo {
public static void main(String[] args) {
Class<?> first = String.class;
// 初始状态:first = String.class。
Class<?> second = Class.forName("java.lang.String");
// 初始状态:second = Class.forName("java.lang.String")。
// 作用:Class<?> second = Class.forName("java.lang.String");;避免把加载时机误当初始化保证。
System.out.println(first == second);
// 输出:true
}
}同一个 bootstrap loader 解析到的 java.lang.String 是同一个 Class;换成两个隔离的自定义加载器后,即使二进制名称一样,也可能得到不同类型身份。
易混点
- 加载、链接和初始化不是一个瞬间;
class字面量通常不执行静态初始化。 - 父委派不是“所有类都由父加载器定义”,父加载失败后子加载器仍可定义自己的类。
- 类名相同不代表类型相同,定义加载器不同就可能 ClassCastException。
- 静态初始化异常不是普通业务异常,后续可能表现为 NoClassDefFoundError。
- 自定义 ClassLoader 的静态缓存、线程和 ThreadLocal 引用会阻止类卸载。
课后小问
为什么 Class.forName(..., false, loader) 适合插件探测? 答案:它可以确认类能否被加载,而不立即执行静态初始化副作用。 解析:真正构造插件时再初始化,并在失败时把配置错误隔离到插件生命周期。
同名类为什么不能直接互相强转? 答案:Class 身份包含定义它的 ClassLoader,两个加载器定义的同名类是不同类型。 解析:共享接口要由共同父加载器定义,插件实现只在各自子加载器内使用。
类初始化失败后为什么后续调用可能变成 NoClassDefFoundError? 答案:JVM 会记录该类初始化失败,后续主动使用不再重复正常初始化。 解析:应查看最初的 ExceptionInInitializerError 根因,而不是只处理后续的 NoClassDefFoundError。
本节小结
- 类加载由加载、验证、准备、解析和初始化组成,静态代码在初始化阶段执行。
- 双亲委派优先复用父加载器定义,类身份由名称和定义加载器共同决定。
- ServiceLoader、ClassValue 和自定义 ClassLoader 都依赖清晰的加载器边界。
- 静态初始化要轻量可控,插件卸载必须断开 ClassLoader 的所有强引用。
快速回顾
- 能按顺序说出加载、链接和初始化的关系。
- 会判断 Class.forName 的初始化开关。
- 能解释双亲委派和同名类强转失败。
- 知道类初始化失败和 ClassLoader 泄漏的诊断方向。
阿源的知识库