我将以一个典型 Java 程序(HelloWorld)的执行过程为基础,逐步分析类加载的每一步,明确涉及的类、方法、输入数据格式、中间数据处理流程,以及最终输出数据格式。
本文从程序启动开始,涵盖类加载的所有阶段(加载、链接、初始化),并具体到每个阶段调用的类和方法。
1. 概述:类加载的背景和流程
        类加载是 Java 虚拟机(JVM)将类文件(.class 文件或字节码)加载到内存,并将其转换为可执行的 java.lang.Class 对象的过程。它是 Java 程序运行的基础,确保程序所需的类在需要时被正确加载、验证和初始化。
类加载的三大阶段
- 加载(Loading):读取类文件的字节码到内存,生成 
Class对象。 - 链接(Linking): 
- 验证(Verification):检查字节码的合法性。
 - 准备(Preparation):为静态变量分配内存并设置默认值。
 - 解析(Resolution):将符号引用转换为直接引用。
 
 - 初始化(Initialization):执行类的静态初始化代码(如静态块和静态变量赋值)。
 
类比:图书馆借书
- 加载:从书库找到一本书(类文件)并放到借阅桌上(内存)。
 - 验证:检查书是否完整、合法(比如没有缺页)。
 - 准备:为书的笔记页(静态变量)分配空白空间。
 - 解析:把书中的参考文献(符号引用)替换为实际地址。
 - 初始化:填写书的笔记页(执行静态代码)。
 
示例程序
假设我们运行一个简单的 Java 程序 Main.java:
public class Main {static int staticVar = 42;static {System.out.println("主程序中的静态块");}public static void main(String[] args) {System.out.println("Hello, World!");StaticTest test= new StaticTest();}
}class StaticTest {static {System.out.println("在StaticTest类中的静态块");}
}
 
我们将以这个程序的执行过程,详细描述类加载的完整流程。
2. 完整类加载流程
以下是类加载的详细步骤,涵盖从程序启动到类加载完成的每一步,明确涉及的类、方法、输入输出数据格式,以及中间处理流程。
2.1 程序启动:JVM 初始化
- 触发点:运行 
java Main命令。 - 涉及类: 
- JVM 内部实现(非 Java 类,由 C++ 实现)。
 java.lang.BootClassLoader(引导类加载器,JVM 内部)。
 - 操作: 
- JVM 启动,初始化引导类加载器(
BootClassLoader)。 BootClassLoader加载核心类库(如java.lang.Object,java.lang.String,java.lang.System)。- JVM 调用 
java.lang.System的initializeSystemClass()方法,设置系统属性和环境。 
 - JVM 启动,初始化引导类加载器(
 - 输入数据格式: 
- 命令行参数:
java Main。 - 核心类库路径(
JAVA_HOME/lib或模块系统中的java.base)。 
 - 命令行参数:
 - 中间处理: 
- JVM 解析命令行,找到主类 
Main。 BootClassLoader读取核心类文件的字节码(.class文件或模块化镜像)。- 核心类的字节码被加载到 JVM 的方法区(Method Area),生成 
java.lang.Class对象。 
 - JVM 解析命令行,找到主类 
 - 输出数据格式: 
- 核心类的 
Class对象,存储在方法区。 - JVM 环境初始化完成,准备加载用户类。
 
 - 核心类的 
 
类比:图书馆开门,主管理员(JVM)先把核心书籍(如字典、工具书)放到桌上(内存),准备好借阅系统。
2.2 加载主类:Main
 
- 触发点:JVM 需要执行 
Main类的main方法。 - 涉及类: 
java.lang.AppClassLoader(应用类加载器)。java.lang.ClassLoader(抽象类,提供加载逻辑)。jdk.internal.loader.BuiltinClassLoader(AppClassLoader的父类)。jdk.internal.loader.URLClassPath(辅助类,管理类路径)。
 - 操作: 
- 查找主类: 
- JVM 通过 
AppClassLoader查找Main类。 AppClassLoader调用父类ClassLoader的loadClass(String name)方法。loadClass实现双亲委派模型:- 先调用 
getParent()获取父加载器(PlatformClassLoader)。 PlatformClassLoader再委托其父加载器(BootClassLoader)。- 如果父加载器找不到类,
AppClassLoader调用自己的findClass(String name)方法。 
- 先调用 
 
 - JVM 通过 
 - 读取字节码: 
AppClassLoader通过BuiltinClassLoader的findClassInModuleOrClassPath方法查找Main.class。URLClassPath从类路径(-cp或默认路径)读取Main.class的字节码。
 - 创建 
Class对象:AppClassLoader调用ClassLoader的defineClass(String name, byte[] b, int off, int len)方法。- JVM 将字节码转换为 
java.lang.Class对象,存储在方法区。 
 
 - 查找主类: 
 - 输入数据格式: 
- 类名:
Main(全限定名Main)。 - 类路径:文件系统路径或 JAR 文件。
 - 字节码:
Main.class文件的二进制数据(字节数组)。 
 - 类名:
 - 中间处理: 
- 双亲委派:确保类只加载一次,避免冲突。 
BootClassLoader检查是否为核心类(不是,失败)。PlatformClassLoader检查是否为平台类(不是,失败)。AppClassLoader从类路径找到Main.class。
 - 字节码读取:
URLClassPath打开文件流,读取Main.class的二进制数据。 - Class 对象生成:JVM 解析字节码,创建 
Class实例,记录类的元信息(如方法表、字段表)。 
 - 双亲委派:确保类只加载一次,避免冲突。 
 - 输出数据格式: 
java.lang.Class对象,表示Main类。- 存储在方法区,包含类的元信息(方法、字段、常量池等)。
 
 
类比:管理员(AppClassLoader)在图书馆(类路径)找到 Main 这本书(.class 文件),检查是否已在核心书库或分馆(父加载器),然后把书的内容(字节码)整理成一本可用的书(Class 对象)。
2.3 链接主类:Main
 
链接阶段将 Main 类的 Class 对象准备好,分为验证、准备和解析三个子阶段。
2.3.1 验证(Verification)
- 触发点:
Main类加载后,JVM 自动验证字节码。 - 涉及类: 
- JVM 内部验证器(非 Java 类)。
 java.lang.ClassLoader(提供上下文)。
 - 操作: 
- JVM 调用内部验证器,检查 
Main类的字节码是否合法。 - 验证内容: 
- 文件格式:是否符合 JVM 规范(魔数 
CAFEBABE、版本号等)。 - 语义:方法和字段的访问权限是否合法。
 - 字节码:指令序列是否安全(无非法跳转)。
 - 符号引用:常量池中的引用是否有效。
 
 - 文件格式:是否符合 JVM 规范(魔数 
 
 - JVM 调用内部验证器,检查 
 - 输入数据格式: 
Main类的字节码(存储在方法区)。
 - 中间处理: 
- JVM 解析字节码的常量池、方法表、字段表。
 - 检查字节码的结构和语义,确保不会导致 JVM 崩溃。
 - 如果验证失败,抛出 
java.lang.VerifyError。 
 - 输出数据格式: 
- 验证通过的 
Class对象,标记为“可继续链接”。 
 - 验证通过的 
 
类比:管理员检查 Main 这本书是否完整(格式正确)、内容合法(没有危险指令),确保它可以安全借阅。
2.3.2 准备(Preparation)
- 触发点:验证通过后,JVM 准备静态变量。
 - 涉及类: 
- JVM 内部实现。
 java.lang.Class(存储静态变量信息)。
 - 操作: 
- JVM 为 
Main类的静态变量分配内存,并设置默认值。 - 示例:
static int staticVar = 42;的staticVar被分配内存,默认值为0(int 的默认值)。 
 - JVM 为 
 - 输入数据格式: 
Main类的Class对象,包含静态变量的元信息。
 - 中间处理: 
- JVM 在方法区为 
staticVar分配 4 字节(int 类型)。 - 设置初始值 
0,暂不执行赋值语句(42在初始化阶段赋值)。 
 - JVM 在方法区为 
 - 输出数据格式: 
Class对象,静态变量内存分配完成,默认值设置。
 
类比:管理员为 Main 书的笔记页(静态变量)分配空白空间,暂时填上默认内容(0)。
2.3.3 解析(Resolution)
- 触发点:JVM 解析 
Main类的符号引用(可选,延迟解析可能在初始化或运行时进行)。 - 涉及类: 
java.lang.ClassLoader(解析引用时可能调用父加载器)。java.lang.Class(存储常量池)。
 - 操作: 
- JVM 解析 
Main类常量池中的符号引用,转换为直接引用。 - 示例:
Main类的main方法调用System.out.println,涉及java.lang.System和java.io.PrintStream。 - JVM 通过 
AppClassLoader加载System和PrintStream类(如果尚未加载)。 
 - JVM 解析 
 - 输入数据格式: 
Main类的常量池,包含符号引用(如CONSTANT_Class_info指向java.lang.System)。
 - 中间处理: 
- JVM 查找符号引用的类(如 
System),调用ClassLoader.loadClass("java.lang.System")。 BootClassLoader加载System类,生成Class对象。- 常量池中的符号引用(如 
#2)被替换为Class对象的内存地址。 
 - JVM 查找符号引用的类(如 
 - 输出数据格式: 
Class对象,常量池中的符号引用更新为直接引用。
 
类比:管理员把 Main 书中的参考书目(符号引用)替换为具体书的地址(直接引用),确保可以快速找到其他书。
2.4 初始化主类:Main
 
- 触发点:JVM 准备执行 
Main.main方法前,初始化Main类。 - 涉及类: 
java.lang.Class(存储静态代码)。- JVM 内部实现(执行初始化)。
 
 - 操作: 
- JVM 执行 
Main类的静态初始化代码:- 静态变量赋值:
staticVar = 42; - 静态块:
System.out.println("主程序中的静态块"); 
 - 静态变量赋值:
 - JVM 调用 
<clinit>方法(类初始化方法,由编译器生成)。 
 - JVM 执行 
 - 输入数据格式: 
Main类的Class对象,包含<clinit>方法。
 - 中间处理: 
- JVM 执行 
<clinit>方法:- 设置 
staticVar = 42。 - 执行静态块,调用 
System.out.println,输出"主程序中的静态块"。 
 - 设置 
 - 如果涉及其他类(如 
System),可能触发它们的加载和初始化。 
 - JVM 执行 
 - 输出数据格式: 
Main类的静态变量初始化完成(staticVar = 42)。- 静态块执行,输出到控制台。
 Class对象标记为“已初始化”。
 
类比:管理员正式填写 Main 书的笔记页(静态变量和静态块),完成书的准备工作。
2.5 执行 main 方法:触发其他类加载
 
- 触发点:JVM 调用 
Main.main(String[] args)。 - 涉及类: 
java.lang.AppClassLoader(加载StaticTest类)。java.lang.ClassLoader(提供加载逻辑)。jdk.internal.loader.BuiltinClassLoader。jdk.internal.loader.URLClassPath。java.lang.Class(表示StaticTest类)。
 - 操作: 
- 执行 
main方法:- 输出 
"Hello, World!"(调用System.out.println)。 - 创建 
StaticTest实例:new StaticTest()。 
 - 输出 
 - 加载 
StaticTest类:new StaticTest()触发StaticTest类的加载。AppClassLoader调用loadClass("StaticTest")。- 类似 
Main类,URLClassPath读取StaticTest.class的字节码。 defineClass创建StaticTest的Class对象。
 - 链接 
StaticTest类:- 验证:检查 
StaticTest字节码的合法性。 - 准备:为 
StaticTest的静态变量分配内存。 - 解析:解析 
StaticTest的符号引用(如System)。 
 - 验证:检查 
 - 初始化 
StaticTest类:- 执行 
<clinit>方法,运行静态块:System.out.println("在StaticTest类中的静态块"); 
 - 执行 
 - 创建 
StaticTest实例:- JVM 调用 
StaticTest的构造方法,分配实例内存,生成对象。 
 - JVM 调用 
 
 - 执行 
 - 输入数据格式: 
- 类名:
StaticTest。 - 字节码:
StaticTest.class文件的二进制数据。 
 - 类名:
 - 中间处理: 
- 加载:
AppClassLoader通过双亲委派查找StaticTest,从类路径加载字节码。 - 链接:验证字节码,分配静态变量内存,解析引用。
 - 初始化:执行静态块,输出 
"在StaticTest类中的静态块"。 - 实例化:分配堆内存,调用构造方法。
 
 - 加载:
 - Output data format: 
StaticTest类的Class对象(方法区)。StaticTest实例(堆内存)。- 控制台输出:
"在StaticTest类中的静态块"。 
 
类比:程序运行时,管理员发现需要 StaticTest这本书,重复“找书 -> 检查 -> 准备 -> 初始化”的过程,最终借出书并创建一本新副本(实例)。
2.6 程序输出
运行 java Main 的完整输出:
主程序中的静态块
Hello, World!
在StaticTest类中的静态块
 
- 解释: 
Main类的静态块在初始化时执行。main方法输出"Hello, World!"。StaticTest类的静态块在new StaticTest()时执行。
 
3. 涉及的类和方法总结
以下是类加载流程中涉及的所有类和关键方法:
| 阶段 | 类 | 方法 | 作用 | 
|---|---|---|---|
| 启动 | JVM 内部实现, BootClassLoader | (无 Java 方法,由 JVM 调用) | 初始化 JVM,加载核心类库(如 java.lang.System)。 | 
| 加载 | AppClassLoader, ClassLoader, BuiltinClassLoader, URLClassPath | loadClass(String), findClass(String), defineClass(String, byte[], int, int), URLClassPath.getResource() | 查找并读取 .class 文件,生成 Class 对象。 | 
| 验证 | JVM 内部, ClassLoader | (无直接 Java 方法,JVM 验证器) | 检查字节码合法性。 | 
| 准备 | JVM 内部, Class | (无直接 Java 方法,JVM 分配内存) | 为静态变量分配内存,设置默认值。 | 
| 解析 | ClassLoader, Class | loadClass(String)(间接调用) | 将符号引用转换为直接引用。 | 
| 初始化 | Class, JVM 内部 | <clinit>(由 JVM 执行) | 执行静态变量赋值和静态块。 | 
| 运行 | AppClassLoader, ClassLoader, Class | loadClass(String), defineClass, <init>(构造方法) | 加载依赖类(如 StaticTest),创建实例。 | 
4. 数据格式总结
| 阶段 | 输入数据格式 | 中间处理 | 输出数据格式 | 
|---|---|---|---|
| 加载 | 类名(String),.class 文件(字节数组) | 双亲委派查找,读取字节码,解析元信息,生成 Class 对象 | java.lang.Class 对象(方法区) | 
| 验证 | 字节码(Class 对象的元信息) | 检查文件格式、语义、字节码指令、符号引用 | 验证通过的 Class 对象 | 
| 准备 | Class 对象(静态变量元信息) | 分配内存,设置默认值(如 int 为 0) | Class 对象(静态变量内存分配完成) | 
| 解析 | 常量池(符号引用) | 加载依赖类,替换符号引用为直接引用(内存地址) | Class 对象(常量池更新) | 
| 初始化 | Class 对象(<clinit> 方法) | 执行静态变量赋值和静态块,可能触发其他类加载 | Class 对象(静态变量初始化完成),控制台输出 | 
| 运行 | 类名,字节码,构造参数 | 加载依赖类,执行构造方法,分配堆内存 | Class 对象,实例对象(堆),控制台输出 | 
5. 补充说明
5.1 双亲委派模型
- 确保类加载的唯一性和安全性。
 - 流程:子加载器(如 
AppClassLoader)先委托父加载器(如PlatformClassLoader,BootClassLoader),只有父加载器失败时才自己加载。 - 涉及方法:
ClassLoader.loadClass和findClass。 
5.2 模块化支持(Java 9+)
PlatformClassLoader和AppClassLoader支持模块系统。java.lang.Module定义模块边界,影响类加载的可见性。- 示例中,
Main和StaticTest可能属于同一模块(如unnamed module)。 
6. 总结
类加载的完整流程从 JVM 启动到程序运行,涉及以下关键步骤:
- 启动:JVM 初始化,
BootClassLoader加载核心类。 - 加载:
AppClassLoader通过loadClass和defineClass加载Main类,生成Class对象。 - 链接: 
- 验证:JVM 检查字节码合法性。
 - 准备:分配静态变量内存,设置默认值。
 - 解析:将符号引用替换为直接引用。
 
 - 初始化:执行 
Main的<clinit>方法,初始化静态变量和块。 - 运行:执行 
main方法,触发StaticTest类的加载、链接、初始化和实例化。 
涉及的核心类:ClassLoader, AppClassLoader, PlatformClassLoader, BootClassLoader, BuiltinClassLoader, URLClassPath, Class, Module。
数据流:从类名和字节码文件开始,经过加载、验证、准备、解析、初始化,最终生成 Class 对象和实例,输出到控制台。
