JVM 是怎么执行Java程序的
假设我们有一个简单的 Java 方法,用来计算两个整数的和:
public int add(int a, int b) {
return a + b;
}
我们将从编写 Java 代码开始,一直到它在 JVM 中执行,详细说明每一步的流转过程。
1. 编写和编译 Java 代码
首先,我们编写 Java 代码并保存为 Add.java
文件。然后,我们使用 javac
编译器将其编译为字节码:
javac Add.java
这会生成一个 Add.class
文件,其中包含 Java 字节码。
2. 加载字节码到 JVM
当我们运行这个类时,JVM 会进行以下步骤:
java Add
3. 类加载器(Class Loader)
JVM 的类加载器会读取 Add.class
文件并加载类定义到内存中。这包括加载方法字节码。
4. 方法区
加载的类和方法字节码存储在 JVM 的方法区中。add
方法的字节码如下所示(假设 Add.class
是如下结构):
public int add(int a, int b);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
5. 执行引擎
解释执行
JVM 的执行引擎开始逐条解释字节码并执行:
- PC寄存器:每个线程有一个独立的 PC 寄存器,指向当前执行的字节码指令。
- 操作数栈:用于存放操作数和中间结果。
- 局部变量表:用于存放方法参数和局部变量。
假设我们调用 add(2, 3)
,执行过程如下:
字节码指令流转
- 指令 0:
iload_1
- 将第一个参数
a
(值为 2)加载到操作数栈。 - 操作数栈:[2]
- 将第一个参数
- 指令 1:
iload_2
- 将第二个参数
b
(值为 3)加载到操作数栈。 - 操作数栈:[2, 3]
- 将第二个参数
- 指令 2:
iadd
- 弹出操作数栈顶的两个值
2
和3
,执行加法操作,结果为5
,然后将结果压回操作数栈。 - 操作数栈:[5]
- 弹出操作数栈顶的两个值
- 指令 3:
ireturn
- 从操作数栈顶弹出值
5
,作为方法返回值。
- 从操作数栈顶弹出值
局部变量表
在调用 add
方法时,局部变量表内容如下:
- 局部变量表:[this, 2, 3]
this
:当前对象引用(如果方法是实例方法)。2
:参数a
的值。3
:参数b
的值。
6. 执行引擎细节
PC 寄存器
每次执行字节码指令时,PC 寄存器指向当前指令的位置:
- 初始时:PC = 0
- 执行
iload_1
后:PC = 1 - 执行
iload_2
后:PC = 2 - 执行
iadd
后:PC = 3 - 执行
ireturn
后:方法结束,返回调用者
7. JIT 编译器
如果 add
方法被频繁调用,JVM 的 JIT 编译器会将字节码编译为本地机器码,以提高执行效率。编译后的机器码直接由 CPU 执行,不再通过解释器逐条解释字节码。
8. 调用者
方法返回值 5
被传递回调用者,调用者继续执行后续代码。
流程图示
Java代码 -> 编译器(javac) -> 字节码(Add.class) -> JVM
-> 类加载器 -> 方法区 -> 执行引擎
-> [PC寄存器, 操作数栈, 局部变量表]
-> 逐条执行字节码(解释器)
-> JIT编译(可选)
-> 本地机器码执行(提高性能)
总结
- 类加载器:加载字节码到内存中。
- 方法区:存储加载的类和方法字节码。
- 执行引擎:解释执行字节码或将其编译为本地机器码。
- PC寄存器:指向当前执行的字节码指令。
- 操作数栈:存放操作数和中间结果。
- 局部变量表:存放方法参数和局部变量。
通过上述详细解释,我们理解了 JVM 中指令流转的全过程。这一过程展示了 CPU、执行引擎、PC 寄存器、操作栈以及机器指令之间的关系。