一个Java的main
方法在JVM中的执行流程可以分为四大阶段 :加载 -> 链接 -> 初始化 -> 执行。
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
String message = "Hello, JVM!";
System.out.println(message);
}
}
第一阶段:加载 (Loading)
目标:找到并加载类的二进制数据。
-
- 编译 :你执行
javac HelloWorld.java
。Java编译器将源代码编译成JVM能理解的字节码,存储在HelloWorld.class
文件中。这个文件包含了一个类常量池(Constant Pool) ,里面有各种符号引用,比如Hello, JVM!
这个字符串的字面量、System
/out
/println
等类名、方法名和字段名。
- 编译 :你执行
-
- 启动JVM :你执行
java HelloWorld
。操作系统会启动JVM进程。
- 启动JVM :你执行
-
- 寻找类 :JVM通过类加载器(ClassLoader) 来加载
HelloWorld
类。
-
• Bootstrap ClassLoader :首先,启动类加载器会去加载JAVA_HOME/lib下的核心类库,如
java.lang
包(包括Object
,String
,System
等)。 -
• Application ClassLoader :然后,应用程序类加载器开始工作,它在你的
CLASSPATH
(默认是当前目录)下寻找HelloWorld.class
文件。
- 寻找类 :JVM通过类加载器(ClassLoader) 来加载
-
- 创建Class对象 :JVM成功读取
HelloWorld.class
的二进制字节流后,会将其转换为方法区(Metaspace) 中的运行时数据结构,并同时在 Java堆(Heap) 中创建一个java.lang.Class
对象,作为方法区这些数据的访问入口。这个Class
对象封装了类的所有元信息(如方法、字段等)。
- 创建Class对象 :JVM成功读取
第二阶段:链接 (Linking)
目标:将加载到方法区的二进制数据合并到JVM运行时状态中。 此阶段细分为三步:
-
- 验证 (Verification) :JVM会严格检查
HelloWorld.class
文件的格式、元数据、字节码等是否符合规范且不会危害JVM自身安全。这是一个非常重要的安全屏障。
- 验证 (Verification) :JVM会严格检查
-
- 准备 (Preparation) :JVM为类的静态变量(static variables) 在方法区分配内存并设置初始值(零值)。注意,这里是初始值,不是代码中赋的值。
- • 例如,如果类里有
static int value = 123;
,在准备阶段,value
会被赋值为0
。真正的赋值123
要等到后面的初始化阶段。
-
- 解析 (Resolution) :JVM将类常量池 中的符号引用(Symbolic References) 替换为直接引用(Direct References)。
-
• 符号引用 :就是一种约定好的形式来表示引用的目标,比如
java/lang/System.out
。 -
• 直接引用:就是一个直接指向目标的指针、偏移量或句柄。
-
• 例如,在这一步,
System.out
这个符号引用会被解析为java.io.PrintStream
对象在堆内存中的实际地址。
第三阶段:初始化 (Initialization)
目标:执行类的构造器 <clinit>()
方法,为静态变量赋予程序设定的初始值。
-
- 到了这一步,JVM才开始真正执行你写在Java代码中的静态语句和静态变量赋值。
-
- JVM会收集类中的所有静态变量的赋值动作 和静态代码块(static {}) ,合并生成一个唯一的
<clinit>()
方法。
- JVM会收集类中的所有静态变量的赋值动作 和静态代码块(static {}) ,合并生成一个唯一的
-
- JVM会确保
<clinit>()
方法在多线程环境下被正确地加锁同步执行,所以类初始化是线程安全的。
- JVM会确保
-
- 在我们的
HelloWorld
例子中,没有静态变量和静态代码块,所以<clinit>()
方法是空的,但这一步依然会发生。
- 在我们的
第四阶段:执行 (Execution & Runtime)
目标:创建线程,执行字节码。
-
- 主线程 :JVM会为
main
方法创建一个主线程 。该线程拥有自己的程序计数器(PC) 和 Java虚拟机栈(JVM Stack)。
- 主线程 :JVM会为
-
- 栈帧 :线程的每个方法调用都会在虚拟机栈中创建一个栈帧(Stack Frame) ,用于存储局部变量表 、操作数栈 、动态链接 、方法返回地址 等信息。
main
方法是程序入口,所以第一个被压入栈的栈帧就是main
方法的栈帧。
- 栈帧 :线程的每个方法调用都会在虚拟机栈中创建一个栈帧(Stack Frame) ,用于存储局部变量表 、操作数栈 、动态链接 、方法返回地址 等信息。
-
- 执行引擎 :JVM的执行引擎 开始解释执行
main
方法栈帧中的字节码。
-
•
String message = "Hello, JVM!";
- • 执行引擎遇到字面量
"Hello, JVM!"
时,会去字符串常量池(String Table,位于堆中) 中寻找。如果找不到,就在堆中创建一个String对象并将其引用驻留在常量池中,然后将该引用存入main
栈帧的局部变量表message
中。
- • 执行引擎遇到字面量
-
•
System.out.println(message);
-
• 执行引擎通过之前在解析阶段 已经转换好的直接引用 ,快速地找到
System.out
对应的PrintStream
对象。 -
• 然后调用该对象的
println
方法,将局部变量message
的引用(指向堆中的String对象)作为参数传入。
-
- 执行引擎 :JVM的执行引擎 开始解释执行
-
- 本地方法调用 :
println
方法底层是一个本地方法(Native Method),调用的是操作系统本身的IO能力,将字符串输出到控制台。
- 本地方法调用 :
-
- 方法返回 :
main
方法执行完毕,其栈帧从虚拟机栈中弹出。主线程结束。
- 方法返回 :
-
- JVM退出 :所有非守护线程都结束后,JVM进程终止。