我将以一个典型 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
对象和实例,输出到控制台。