Skip to content

类加载五阶段完整原理

JVM不能直接把磁盘上的.class文件当作可执行Java对象。它必须把字节流转换为受JVM约束的运行时类型,验证其安全性,为静态状态准备内存,把常量池中的符号引用连接到真实目标,最后执行类初始化逻辑。

这条链路通常概括为:加载、验证、准备、解析、初始化。验证、准备、解析合称连接。

mermaid
flowchart TD
    A["Loading:取得Class字节并定义运行时类"] --> B["Verification:验证格式、类型和字节码"]
    B --> C["Preparation:为静态字段准备存储与初始值"]
    C --> D["Resolution:符号引用转为可定位目标"]
    D --> E["Initialization:执行clinit"]
    E --> F["创建实例、调用方法、访问字段"]

学习目标

学完后应能解释:

  • Loading阶段怎样从类名找到字节流,loadClassdefineClass分别做什么。
  • 为什么类身份不仅是全限定名,还必须包含定义它的ClassLoader。
  • 数组类为什么没有普通.class文件。
  • java.lang.Class对象、HotSpot类元数据、JDK 7永久代和JDK 8元空间的关系。
  • 验证为什么分为文件格式、元数据、字节码和符号引用验证。
  • JDK 7 StackMapTable怎样帮助类型检查,为什么不能把验证理解成“重新执行一遍程序”。
  • 准备阶段的零值、ConstantValue和初始化阶段显式赋值有什么区别。
  • 符号引用、直接引用、解析、链接和动态分派的区别。
  • invokevirtual为什么解析了方法符号后仍可能在运行时选择子类实现。
  • <clinit>怎样生成、按什么顺序执行、为什么初始化失败后不能自动重试。
  • ClassNotFoundException、NoClassDefFoundError、ClassFormatError、UnsupportedClassVersionError、VerifyError、IllegalAccessError分别可能发生在哪一段。
  • 如何通过javap -verbose把源码、常量池、字段属性和字节码连接起来。

一、先建立完整运行链路

以执行new OrderService()为例:

mermaid
flowchart TD
    A["字节码执行到new符号引用"] --> B["确认OrderService是否已加载"]
    B --> C["选择ClassLoader并取得字节流"]
    C --> D["defineClass创建运行时类"]
    D --> E["验证Class格式和字节码"]
    E --> F["准备静态字段存储"]
    F --> G["解析必要的类/字段/方法引用"]
    G --> H["执行父类与本类clinit"]
    H --> I["在堆中分配OrderService对象"]
    I --> J["执行实例初始化与构造器"]

如果类已经由当前运行时类型体系加载、连接、初始化,前半段不会重复执行。每个阶段都有缓存和状态,不是每次new都重新读取JAR。

二、加载阶段到底做什么

规范层面可以概括为三件事:

  1. 根据类的二进制名取得表示该类型的二进制字节流。
  2. 由字节流创建JVM内部的运行时类数据结构。
  3. 创建代表该类型的java.lang.Class镜像,作为Java代码访问类型信息的入口。

2.1 字节流不一定来自磁盘class文件

可能来源于:

  • 应用目录中的.class
  • JAR/WAR/ZIP中的条目。
  • 模块运行时镜像或JDK启动类路径。
  • 网络、数据库或加密包。
  • JSP等模板编译结果。
  • Java动态代理、CGLIB、Byte Buddy、ASM生成的字节。
  • Agent或Instrumentation转换后的字节。

JVM关心的是最终字节是否符合Class File格式,不要求它一定以普通文件长期存在。

2.2 loadClass、findClass与defineClass

典型自定义ClassLoader链路:

mermaid
flowchart TD
    A["调用loadClass(name)"] --> B["findLoadedClass检查是否已加载"]
    B --> C{"已加载?"}
    C -- "是" --> D["返回已有Class"]
    C -- "否" --> E["按委派规则请求父加载器"]
    E --> F{"父加载成功?"}
    F -- "是" --> D
    F -- "否" --> G["当前加载器findClass"]
    G --> H["读取/生成字节"]
    H --> I["defineClass"]
    I --> D
  • loadClass负责缓存检查、委派策略和总流程。
  • findClass通常由子类实现“怎样找到字节”。
  • defineClass把字节交给JVM定义运行时类,并触发必要格式检查。

只重写findClass通常没有打破父优先,因为标准loadClass仍先委派父加载器。改变父子查找顺序通常要有选择地重写loadClass,详见类加载器与双亲委派

2.3 发起加载器与定义加载器

  • 发起加载器:发起某次加载请求的加载器。
  • 定义加载器:最终调用defineClass定义该类型的加载器。

父加载器定义的类可以由子加载器发起请求获得。类型身份由二进制名和定义加载器共同决定:

text
运行时类型身份 = binary name + defining ClassLoader

两个ClassLoader分别定义com.example.Plugin,即使字节完全相同,JVM也把它们视为两个不同类型。强转可能抛:

text
ClassCastException: com.example.Plugin cannot be cast to com.example.Plugin

错误信息左右类名相同并不矛盾,定义加载器不同。

2.4 数组类怎样创建

数组类型没有普通Class文件,不由ClassLoader读取[L...;.class。JVM在首次需要时直接创建数组类:

  • 引用类型数组的组件类型由其定义加载器加载,数组类与组件类型的加载器关联。
  • 基本类型数组不依赖应用ClassLoader定义组件类。
  • 创建Order[]不会创建Order实例,也不会因此执行Order的<clinit>

2.5 Class对象到底放在哪里

必须区分两个层次:

内容HotSpot中的概念位置
字段表、方法表、运行时常量池、继承信息等JVM类元数据JDK 7常由永久代等实现承载;JDK 8主要迁移到本地内存Metaspace
java.lang.Class镜像对象Java堆中的普通可管理对象

因此“Class对象存放在方法区”是不准确的。方法区是JVM规范的逻辑概念;HotSpot用永久代、元空间等实现类元数据,而Java代码拿到的Class镜像仍是堆对象,并关联底层类元数据。

2.6 加载阶段常见失败

错误常见含义
ClassNotFoundException代码主动按名称加载,加载器搜索后找不到;它是受检异常
NoClassDefFoundErrorJVM执行已有符号依赖时无法得到类定义,或该类之前初始化失败
ClassFormatError字节流不符合Class File格式
UnsupportedClassVersionErrorClass主版本高于当前JVM支持范围,是ClassFormatError子类
SecurityException例如普通加载器试图定义java.*受保护包、签名/包约束冲突
LinkageError同一加载器重复定义、加载约束冲突等链接问题的错误家族

ClassNotFoundException与NoClassDefFoundError不能只靠中文“找不到类”合并回答,触发方式和排查方向不同。

三、验证阶段为什么存在

Java源码由javac生成并不代表运行时字节一定可信。Class可能来自网络、Agent转换、动态生成器、损坏JAR,甚至恶意输入。JVM必须在不破坏自身内存安全和类型安全的前提下执行它。

验证通常分为四类逻辑工作,它们可能与加载、解析交叠,不要求日志中一定逐项打印四个阶段。

3.1 文件格式验证

检查字节流能否按Class File结构解释,例如:

  • 魔数是否为0xCAFEBABE
  • 主次版本是否受当前JVM支持。
  • 常量池计数、索引和Tag是否合法。
  • UTF-8常量、字段表、方法表、属性长度是否完整。
  • 文件是否被截断或有多余非法结构。

这一层失败常见ClassFormatErrorUnsupportedClassVersionError

3.2 元数据验证

对类型声明做语义检查,例如:

  • 除Object外是否存在合法父类。
  • 是否继承了final类。
  • 非抽象类是否实现所需抽象方法。
  • 字段、方法覆盖和访问标志是否符合约束。
  • 类/接口语义组合是否合理。

3.3 字节码验证

它对方法Code属性进行数据流和控制流分析,目标包括:

  • 操作数栈类型与指令要求匹配。
  • 局部变量使用前已赋值且类型正确。
  • 跳转目标落在合法指令边界。
  • 方法返回指令与声明返回类型一致。
  • 对象初始化前后的未初始化引用使用合法。
  • 不允许把任意整数当对象地址直接访问JVM内存。

验证器不是“真正执行所有业务路径”。它根据字节码结构和类型状态证明关键安全性质;业务空指针、数组越界、SQL错误仍然可能在运行时发生。

3.4 JDK7/8与StackMapTable

现代Class文件常在方法Code属性中包含StackMapTable,记录某些字节码偏移处局部变量表和操作数栈的类型状态。JDK 7开始新的类型检查验证方式成为重要主线,验证器不必对所有路径从头反复推导,能结合这些栈映射帧验证控制流合并点。

字节码生成工具若计算了错误的StackMap Frame,即使指令看似合理,也可能在JDK 7/8加载时报VerifyError。升级ASM、CGLIB、代理框架时必须考虑目标Class版本和Frame计算。

3.5 符号引用验证

在解析具体类、字段和方法符号时还要验证:

  • 目标类能否找到。
  • 目标字段或方法是否存在。
  • 当前访问方是否有权限。
  • 解析结果是否满足静态/实例、类/接口等约束。

失败可能出现NoSuchFieldErrorNoSuchMethodErrorIllegalAccessErrorIncompatibleClassChangeError等。

这些错误常说明“编译时依赖版本”和“运行时实际加载版本”不一致,而不是普通业务异常。

四、准备阶段:静态字段先得到什么值

准备阶段为类的静态字段建立存储并设置准备值。实例字段属于每个对象,不在类准备阶段分配。

4.1 普通静态字段先是零值

java
static int timeout = 30;
static Object client = createClient();

准备阶段近似:

text
timeout = 0
client = null

30createClient()在初始化阶段的<clinit>中执行。

4.2 ConstantValue例外

java
static final int MAX_RETRY = 3;
static final String APP = "order";

如果字段带ConstantValue属性,JVM可在准备阶段按该属性设置值。常见于满足条件的static final基本类型和String常量。

4.3 final不等于ConstantValue

java
static final Integer BOXED = Integer.valueOf(3);
static final int RANDOM = new java.util.Random().nextInt();

右侧需要运行代码,不会简单成为ConstantValue;准备阶段仍先得到null0,初始化阶段再执行方法调用和赋值。

4.4 用javap验证字段属性

bash
javap -verbose -p ClassLoadingProcessDemo

观察字段条目是否出现:

text
ConstantValue: int 3

不要仅根据源码里出现final猜测。

五、解析阶段:名字怎样变成可访问目标

编译后的Class常量池保存的是符号引用,例如:

text
类:com/example/PaymentService
字段:timeout I
方法:pay (Ljava/lang/String;)V
接口方法:execute ()V

符号引用不要求编译时知道目标内存地址。解析把它转换为JVM能定位目标类、字段或方法的运行时结构。

5.1 常见解析对象

  • 类和接口。
  • 字段。
  • 类方法。
  • 接口方法。
  • 方法类型与方法句柄。
  • JDK 7的invokedynamic调用点相关常量。

5.2 解析可以延迟

JVM可以在连接时积极解析,也可以在首次执行某条相关指令时懒解析。只要遵守规范的错误抛出和可见行为约束,具体时机由实现决定。

因此“解析一定完整发生在初始化之前”过于机械。生命周期顺序描述的是阶段开始约束和逻辑关系,部分符号可以延迟解析。

5.3 解析不等于动态分派

java
Animal animal = new Dog();
animal.speak();

解析可以确定speak符号所属的方法族、签名和可访问性,但invokevirtual在运行时仍根据接收者实际类型Dog选择覆盖实现。

mermaid
flowchart TD
    A["常量池Methodref: Animal.speak"] --> B["解析类、名称、描述符和访问权限"]
    B --> C["运行到invokevirtual"]
    C --> D["读取接收者实际类型Dog"]
    D --> E["方法表/内联缓存等选择Dog.speak"]

需要区分:

  • invokestatic:静态方法调用,没有接收者动态分派。
  • invokespecial:构造器、私有方法、特定super调用等特殊调用。
  • invokevirtual:普通类实例方法,通常需要虚方法分派。
  • invokeinterface:接口方法调用。
  • invokedynamic:JDK 7引入,由Bootstrap Method建立动态调用点;JDK 8 Lambda常使用它。

解析把符号连接到合法运行时目标体系,不代表所有调用都在解析时固定最终实现。

六、初始化阶段:执行clinit

初始化时机详见类加载与初始化时机。本节关注五阶段中的具体动作。

6.1 clinit怎样生成

编译器按源码顺序收集:

  • 静态字段的运行时赋值。
  • static {}中的语句。

合成<clinit>()V。例如:

java
static int a = 1;
static {
    a = 2;
}
static int b = a + 1;

近似字节码顺序:

text
iconst_1 → putstatic a
iconst_2 → putstatic a
getstatic a → iconst_1 → iadd → putstatic b
return

最终a=2b=3

6.2 非法向前引用

java
static {
    value = 1;                 // 可以赋值
    // System.out.println(value); // 编译错误:非法向前引用
}
static int value = 2;

源码规则限制在声明前读取字段,但允许某些赋值。即使绕过源码编译器观察字节码,也不能忽视Java语言层约束。

6.3 父类和接口顺序

  • 普通类初始化前先初始化所需父类。
  • JDK 7没有default接口方法。
  • JDK 8初始化类时还要处理声明default方法的相关超接口。
  • 初始化一个接口本身通常不要求先初始化所有父接口,除非实际主动使用。

6.4 同步和错误状态

每个运行时类在初始化时有同步和状态管理:未初始化、正在由某线程初始化、已初始化、初始化错误。其他线程主动使用正在初始化的类会等待;失败后同一ClassLoader中的该类不会自动重试。

首次失败常见ExceptionInInitializerError,后续使用常见NoClassDefFoundError: Could not initialize class

七、一个字段从源码到运行值的全过程

java
class Configuration {
    static final int COMPILE_TIME = 3;
    static int timeout = loadTimeout();
}

COMPILE_TIME

text
javac生成字段ConstantValue=3
→ Loading读取字段表
→ Verification验证ConstantValue类型合法
→ Preparation按ConstantValue设置3
→ 调用方还可能把3内联进自己的字节码

timeout

text
javac把loadTimeout与putstatic放进clinit
→ Loading读取字段与clinit字节码
→ Verification验证调用与赋值类型
→ Preparation先把timeout设为0
→ Resolution解析loadTimeout方法引用
→ Initialization执行loadTimeout并putstatic真实结果

这就是为什么“准备阶段静态变量赋值”不能笼统说成“执行源码右侧值”。

八、JDK7/8可运行Demo

java
public class ClassLoadingProcessDemo {
    static final int CONSTANT = 3;
    static final Integer BOXED = Integer.valueOf(4);
    static int value = initializeValue();

    static {
        value = value + 10;
        System.out.println("clinit value=" + value);
    }

    private static int initializeValue() {
        System.out.println("initializeValue");
        return 5;
    }

    public static void main(String[] args) {
        System.out.println("CONSTANT=" + CONSTANT);
        System.out.println("BOXED=" + BOXED);
        System.out.println("value=" + value);
    }
}

编译运行:

bash
javac ClassLoadingProcessDemo.java
java ClassLoadingProcessDemo

预期顺序:

text
initializeValue
clinit value=15
CONSTANT=3
BOXED=4
value=15

原因:JVM启动主类触发初始化;BOXEDvalue的运行时赋值按源码顺序进入<clinit>,然后执行static块,最后才进入main

九、javap实验:逐项验证

bash
javap -verbose -p ClassLoadingProcessDemo
javap -c -p ClassLoadingProcessDemo

检查:

  1. 主版本号major version,JDK 7通常51,JDK 8通常52。
  2. 常量池中的Class、Fieldref、Methodref和String。
  3. CONSTANT字段的ConstantValue: int 3
  4. BOXED没有整数ConstantValue,而是在<clinit>调用Integer.valueOfputstatic
  5. value先调用initializeValueputstatic
  6. static块的加法和打印继续位于同一个<clinit>
  7. 方法Code中的max_stackmax_locals和可能的StackMapTable。

9.1 Class版本快速判断

Java常见Class主版本
Java 751
Java 852
Java 1155
Java 1761
Java 2165

高版本编译、低版本运行时常见:

text
UnsupportedClassVersionError

修复方向是统一编译source/target--release、运行JDK和依赖版本,不是删除class文件碰运气。

十、错误类型与阶段映射

现象主要阶段/边界重点检查
ClassNotFoundException按名称加载类名、请求ClassLoader、classpath/JAR
NoClassDefFoundError: X加载/链接依赖编译时有、运行时缺,依赖排除或加载器可见性
NoClassDefFoundError: Could not initialize class X之前初始化失败找第一次ExceptionInInitializerError和cause
UnsupportedClassVersionError格式验证Class major version与运行JDK
ClassFormatError文件格式验证损坏JAR、错误字节生成器、传输截断
VerifyError字节码验证Agent、ASM/CGLIB版本、StackMap Frame、目标JDK
NoSuchMethodError符号解析/链接编译与运行依赖版本冲突
NoSuchFieldError符号解析/链接字段在运行时版本不存在
IllegalAccessError符号解析/访问约束运行时版本改变可见性、模块/加载器边界
IncompatibleClassChangeError类型/成员形态不兼容类变接口、静态变实例等二进制不兼容
ExceptionInInitializerError初始化static字段和static块底层异常

NoSuchMethodException是反射API的受检异常;NoSuchMethodError是已编译字节码链接不到运行时方法的Error,两者也不能混淆。

十一、商业场景:依赖冲突导致NoSuchMethodError

11.1 现象

订单服务编译时使用json-client 2.0

java
client.writeValue(order, options);

部署包中却被另一个依赖带入json-client 1.5,该版本没有这个方法。类可能成功加载,但执行到方法符号解析/调用时抛NoSuchMethodError

11.2 为什么编译通过仍会线上失败

mermaid
flowchart TD
    A["编译classpath包含2.0"] --> B["字节码写入2.0方法符号引用"]
    B --> C["运行classpath实际先加载1.5"]
    C --> D["解析writeValue新签名"]
    D --> E["1.5中不存在"]
    E --> F["NoSuchMethodError"]

编译器只证明编译classpath可用,不能保证最终Fat JAR、容器或应用服务器实际加载同一版本。

11.3 取证

bash
mvn dependency:tree
gradle dependencies
jar tf app.jar
javap -classpath actual-json-client.jar -p com.example.JsonClient

JDK 8可通过:

bash
-verbose:class

观察类从哪个JAR加载。JDK 9+可使用统一日志:

bash
-Xlog:class+load=info

11.4 修复

  • 在构建系统统一依赖版本并解释每个exclude。
  • 对最终可部署产物做依赖和重复类检查,不只检查IDE。
  • 在与生产相同ClassLoader模型中做启动和关键接口测试。
  • 记录镜像、JAR哈希和构建依赖清单。

十二、商业场景:Agent生成字节码出现VerifyError

监控Agent在类加载时转换方法,旧版字节码库没有正确生成JDK 8所需StackMap Frame。应用源码和原始JAR都正常,但启用Agent后类验证失败。

排查顺序:

  1. 保存完整VerifyError消息、目标类和字节码偏移。
  2. 记录JDK完整版本、Agent版本和启动参数。
  3. 在隔离环境对比启用/禁用Agent,不能直接在生产长期裸奔。
  4. 导出转换前后Class,用javap -verbose查看版本、Code和StackMapTable。
  5. 升级兼容目标JDK的Agent/ASM版本并回归测试。

不要用-noverify作为生产修复。关闭验证会削弱JVM安全与类型保证,也可能把问题推迟成更危险故障。

十三、类卸载怎样发生

类卸载主要针对自定义ClassLoader定义的类型。一个类“没有实例”通常不足以卸载,还要使:

  1. 定义它的ClassLoader不再可达。
  2. 该加载器定义的Class对象不再被强引用。
  3. 这些类的实例不再可达。
  4. JVM执行了涉及类卸载的GC周期,并且收集器/参数允许类卸载。
mermaid
flowchart TD
    A["插件ClassLoader"] --> B["定义多个插件Class"]
    B --> C["Class创建实例和静态状态"]
    C --> D{"卸载插件后还有引用?"}
    D -- "线程/TCCL/缓存仍引用" --> E["ClassLoader不能回收"]
    D -- "无引用" --> F["ClassLoader、Class、实例可达性消失"]
    F --> G["满足条件的GC卸载类元数据"]

热部署、脚本、动态代理和插件系统若用静态集合、ThreadLocal、线程上下文类加载器、JDBC驱动或日志框架长期引用旧加载器,会导致JDK 7 PermGen或JDK 8 Metaspace持续增长。

十四、生产排查Runbook

14.1 先保存完整错误链

不要只截最后一行。至少保存:

  • 最外层异常和所有Caused by
  • 第一次出现时间、发布版本和实例。
  • 完整类名、方法描述符、Class版本信息。
  • 启动命令、JDK完整版本和实际部署包。

14.2 判断是哪一段

mermaid
flowchart TD
    A["类相关错误"] --> B{"错误家族"}
    B -- "找不到类" --> C["加载器、classpath、依赖可见性"]
    B -- "版本/格式/Verify" --> D["Class版本、字节生成器、Agent"]
    B -- "NoSuchMethod/Field" --> E["编译与运行依赖不一致"]
    B -- "Initializer" --> F["static初始化底层cause"]
    B -- "Metaspace增长" --> G["ClassLoader泄漏和动态类数量"]

14.3 常用证据

bash
java -version
jcmd <pid> VM.command_line
jcmd <pid> VM.classloader_stats
jmap -clstats <pid>
jstat -class <pid> 1000 10
javap -verbose -p -classpath target.jar com.example.Target
jar tf target.jar

JDK 7/8具体工具参数随更新版和发行版不同,先运行jcmd <pid> helpjmap -h确认支持范围。

十五、常见误区

误区正确理解
类加载就是把class读进内存还包含运行时定义、连接和初始化等不同状态
Class对象在元空间Class镜像是堆对象,底层类元数据在永久代/元空间等实现区域
同名类就是同一类型还要看定义ClassLoader
验证能发现所有业务Bug它保证关键格式和类型安全,不执行所有业务路径
准备阶段把static int x=5设成5普通字段先为0,5通常在clinit赋值
所有static final都在准备阶段是最终值只有符合ConstantValue条件的字段
解析后虚方法实现完全固定invokevirtual仍根据接收者实际类型动态分派
初始化失败后重试请求即可同一运行时类进入错误状态,不自动重跑clinit
NoSuchMethodError是方法没写常见根因是运行时加载了错误依赖版本
没有对象实例类就会卸载定义ClassLoader、Class镜像和其他引用也必须不可达

十六、面试标准回答

类加载有哪五个阶段

Loading根据二进制名取得Class字节并由定义加载器创建运行时类和Class镜像;Verification检查格式、类型、字节码和符号访问安全;Preparation为静态字段建立存储并设置零值或ConstantValue;Resolution把常量池符号引用连接到可定位的类、字段和方法;Initialization按规则执行父类、本类的clinit。解析可按实现延迟,五阶段不能机械理解成完全不重叠的五段时间。

准备和初始化有什么区别

准备阶段不执行普通Java初始化代码,普通静态字段先得到0、false或null;带合法ConstantValue的static final基本类型/String可直接得到常量值。初始化阶段执行编译器合成的clinit,完成方法调用、对象创建、普通静态字段显式赋值和static块。

为什么同名类会ClassCastException

JVM运行时类型身份是二进制名加定义ClassLoader。两个插件加载器分别defineClass同名Plugin,它们是两个不同类型;即使字节相同也不能直接互相强转。应把共享API放到共同父加载器,插件只加载实现,并避免重复打包接口类。

NoSuchMethodError为什么编译时没有发现

编译时classpath中存在目标方法,所以字节码成功生成符号引用;运行时却加载了另一个旧版本类,解析该名称和描述符时找不到方法,于是抛NoSuchMethodError。要检查最终部署包、依赖树和实际类加载来源,而不是只看IDE依赖。

Class对象和Metaspace是什么关系

Java代码拿到的java.lang.Class镜像是堆对象,它关联HotSpot内部类元数据;JDK7常由永久代承载大量类元数据,JDK8主要迁到本地内存Metaspace。ClassLoader泄漏会让Class镜像、类元数据和相关静态对象一起无法回收。

十七、关联知识点

十八、学习验收

  • [ ] 能画出Loading、Verification、Preparation、Resolution、Initialization完整链路。
  • [ ] 能解释loadClassfindClassdefineClass分别负责什么。
  • [ ] 能说明类身份为什么包含定义ClassLoader。
  • [ ] 能纠正“Class对象存放在元空间”的错误说法。
  • [ ] 能从ConstantValue判断准备阶段字段值。
  • [ ] 能解释StackMapTable与JDK7/8字节码验证关系。
  • [ ] 能区分解析方法符号与invokevirtual动态分派。
  • [ ] 能把ClassNotFoundException、VerifyError、NoSuchMethodError和初始化失败映射到正确排查方向。
  • [ ] 能使用javap、依赖树和类加载日志证明运行时依赖冲突。
  • [ ] 能解释ClassLoader泄漏为什么导致PermGen/Metaspace增长。

本章小结

类加载五阶段把“不可信的符号字节”逐步变成“可安全执行、具有静态状态的运行时类型”。加载决定字节和类型身份,验证守住格式与类型安全,准备建立静态存储,解析连接符号,初始化执行Java静态逻辑。真正理解后,线上看到的不再只是“类找不到”,而能从错误家族、ClassLoader、Class版本、常量池和首次初始化异常准确定位故障发生在哪一层。