类加载把 class 字节码变成 JVM 可使用的类型。基础面试要分清加载与初始化,并说明双亲委派怎样减少核心类被随意替换的风险。
五个阶段
加载 → 验证 → 准备 → 解析 → 初始化 └──────── 连接 ────────┘| 阶段 | 主要工作 |
|---|---|
| 加载 | 获取类的二进制表示,创建对应类型的运行时表示和 Class 对象 |
| 验证 | 检查字节码及相关结构是否符合要求 |
| 准备 | 为静态字段安排存储并设置初始值,普通字段通常先取零值 |
| 解析 | 将符号引用转换为对应运行时实体的引用 |
| 初始化 | 执行类初始化逻辑,包括静态字段初始化表达式与静态代码块 |
这些是逻辑阶段,不代表所有解析一定在初始化前一次性完成,部分解析可以推迟。被 ConstantValue 标记的静态常量在准备阶段的处理也与普通静态字段不同,基础上知道有这个例外即可。
什么时候初始化?
常见主动使用包括 new 实例、调用类声明的静态方法、访问或修改类声明的非编译期常量静态字段,以及某些反射操作。初始化类时通常先初始化其父类。
访问编译期常量可能不触发声明类初始化,因为使用方已把常量编入自己的字节码。通过子类名访问父类声明的静态字段,通常只触发声明它的父类初始化,而不是子类。
类初始化与对象初始化不同:静态初始化通常每个类身份一次,实例构造在每次创建对象时执行。
双亲委派是什么?
常见 ClassLoader.loadClass 实现先检查是否已经加载,再让父加载器尝试,父加载器无法加载时自己再找。这里的父关系通常是委托关系,不是 Java 类继承关系。
应用加载器 → 上层加载器 → 启动加载器 先向上请求,无法加载时再逐层尝试好处是减少重复定义,让核心类优先由可信加载器加载。它是常见类加载器机制,不是所有自定义加载器不可改变的绝对规则。
JDK 8 常用“启动、扩展、应用”描述;JDK 9+ 常用“启动、平台、应用”描述。面试说明版本即可,不要把扩展加载器当作所有版本都存在的组件。
什么决定类的身份?
类的完整名称与定义它的类加载器共同决定 JVM 中的类身份。同名类由不同定义加载器加载,可能是不同类型,不能随意互相强转。
线程上下文类加载器允许库代码使用调用环境指定的加载器查找实现,例如 SPI 场景。只需理解它为何存在,不需要第一轮展开容器隔离和热部署。
面试回答模板
类加载包含加载、验证、准备、解析和初始化,连接包括中间三个阶段。准备处理静态字段初始状态,初始化执行静态初始化逻辑。双亲委派一般先交给上层加载器,无法加载再自己尝试,减少核心类替换与重复定义。JVM 的类身份由名称与定义加载器共同决定。
自测
- 加载了类是否意味着静态代码块已执行?不一定,加载与初始化是不同阶段。
- 两个同名类一定是同一种类型吗?不一定,还要看定义加载器。
- JDK 17 还应统一称上层加载器为扩展加载器吗?不应,应注意平台加载器等版本变化。