类加载过程包含加载、验证、准备、解析、初始化五个阶段,其中加载将字节码读入内存并创建class对象,验证确保字节码安全,准备为静态变量分配内存并设默认值,解析将符号引用转为直接引用,初始化执行静态代码块和静态变量赋值。

类的加载过程是 JVM 将 .class 字节码变成可运行的 Java 类的关键路径,五个阶段各有明确分工,不是简单线性执行,而是常交叉进行。理解每个阶段“做什么”和“不做什么”,才能避开常见误区(比如误以为准备阶段会执行 static 代码块)。
加载:把字节码搬进内存,建一个 Class 对象
这是入口动作,由类加载器完成,核心就三件事:
- 根据全限定名(如 java.lang.String)找到并读取对应的二进制字节流——来源可以是本地文件、JAR 包、网络、甚至运行时动态生成(如代理类)
- 把字节流解析成方法区里的运行时数据结构(比如常量池、字段表、方法表等)
- 在堆中创建一个 java.lang.Class 实例,作为访问方法区数据的统一入口
注意:此时 Class 对象已存在,但类还没“活”起来——静态变量还是默认值,static 块一句都没执行。
验证:给字节码做四层安全体检
防止恶意或错误的 class 文件危害 JVM,分四个层次检查:
- 文件格式验证:魔数是不是 0xCAFEBABE?主次版本号是否支持?结构是否合规?
- 元数据验证:语义是否合法?比如 final 类被继承、父类缺失、抽象方法没实现
- 字节码验证:指令流是否安全?有没有跳转到方法外、类型转换是否越界
- 符号引用验证:后面要解析的类、字段、方法名是否存在、权限是否允许访问
这个阶段可跳过(如用 -Xverify:none),但生产环境一般不建议关。
准备:只给 static 变量分配内存并设零值
重点在“只做两件事”,且有明确限制:
- 只为 类变量(即 static 修饰的变量) 分配内存(在方法区),不包括实例变量
- 赋的值是该类型的默认零值:int 是 0,boolean 是 false,引用类型是 null
- 例外:如果变量被 static final 修饰且是编译期常量(如 public static final int PORT = 8080;),则直接赋真实值,跳过零值阶段
此时仍不会执行任何 Java 代码,static {} 和 static int x = getValue(); 都还没动。
解析:把“名字”换成“地址”,打通调用链路
符号引用是字符串形式的逻辑描述(如 “java/lang/Object”、“testMethod:(I)V”),解析就是把它转成 JVM 能直接使用的内存地址引用:
- 类或接口的解析:确认类是否存在、能否访问(比如包权限、private 类不能被外部引用)
- 字段解析:定位字段在类实例或静态存储中的偏移量
- 方法解析:区分是实例方法、静态方法还是接口方法,填入虚方法表或接口方法表
- 接口方法解析:专门处理 interface 中 default/static 方法的绑定
解析不一定在初始化前完成,某些情况(如多态调用)会延迟到首次使用时才解析,这就是支持动态绑定的基础。
初始化:真正执行 Java 代码,完成类的“成人礼”
这是真正运行 Java 逻辑的阶段,执行的是编译器生成的
- 合并所有 static 变量的赋值语句 和 static {} 代码块,按源码顺序执行
- 如果类有父类且父类未初始化,则先触发父类的初始化(接口除外,接口初始化不强制初始化父接口)
- 整个过程由 JVM 保证线程安全:多个线程同时触发初始化,只有一个能执行
() ,其余阻塞等待
一旦完成,类才算真正“准备好”了——静态字段有了初始值,静态逻辑已运行,后续 new 实例或调用静态方法都可正常进行。










