JVM 架构 101:了解虚拟机
Java 应用程序无处不在,它们存在于我们的手机、平板电脑和计算机上。在许多编程语言中,这意味着需要多次编译代码才能使其在不同的操作系统上运行。对于我们开发者来说,Java 最酷的地方或许在于它被设计为平台无关的(正如那句老话所说,“一次编写,随处运行”),因此我们只需编写和编译一次代码。
这怎么可能呢?让我们深入研究一下 Java 虚拟机 (JVM) 来找出答案。
JVM 架构
这听起来可能令人惊讶,但 JVM 本身对 Java 编程语言一无所知。相反,它知道如何执行自己的指令集,称为Java 字节码,该指令集以二进制类文件的形式组织。Java 代码由javac命令编译为 Java 字节码,然后在运行时由 JVM 将其转换为机器指令。
线程
Java 的设计目标是并发,这意味着可以通过在同一进程中运行多个线程来同时执行不同的计算。当一个新的 JVM 进程启动时,JVM 中会创建一个新的线程(称为主线程)。代码从这个主线程开始运行,并可以生成其他线程。实际的应用程序可能拥有数千个运行的线程,它们用于不同的目的。有些线程用于处理用户请求,有些线程用于执行异步后端任务,等等。
堆栈和框架
每个 Java 线程都会创建一个框架堆栈,用于保存方法框架并控制方法的调用和返回。方法框架用于存储其所属方法的数据和部分计算。当方法返回时,其框架会被丢弃。然后,其返回值会被传递回调用者框架,调用者框架现在可以使用它来完成自身的计算。
JVM 执行方法的平台是方法框架。方法框架主要由两部分组成:
- 局部变量数组——存储方法的参数和局部变量
- 操作数栈——执行方法计算的地方
工作原理
让我们通过一个简单的例子来理解各个元素是如何协同运行程序的。假设我们有一个简单的程序,计算 2 + 3 的值并打印结果:
class SimpleExample {
public static void main(String[] args) {
int result = add(2,3);
System.out.println(result);
}
public static int add(int a, int b) {
return a+b;
}
}
要编译这个类,我们运行javac SimpleExample.java,编译后会生成文件SimpleExample.class。我们已经知道这是一个包含字节码的二进制文件。那么如何检查这个类的字节码呢?使用javap。
javap是 JDK 自带的命令行工具,可以反汇编类文件。调用javap -c -p会打印出类的反汇编字节码(-c),包括私有(-p)成员和方法:
Compiled from "SimpleExample.java"
class SimpleExample {
SimpleExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: iconst_3
2: invokestatic #2 // Method add:(II)I
5: istore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: iload_1
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
13: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
那么,运行时 JVM 内部会发生什么呢?java SimpleExample启动了一个新的 JVM 进程,并创建了主线程。为 main 方法创建一个新的栈帧,并将其压入线程堆栈。
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: iconst_3
2: invokestatic #2 // Method add:(II)I
5: istore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: iload_1
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
13: return
main 方法有两个变量:args和result。它们都位于局部变量表中。main 函数的前两个字节码命令iconst_2和iconst_3分别将常量值 2 和 3 加载到操作数栈中。下一个命令invokestatic调用静态方法 add。由于此方法需要两个整数作为参数,因此invokestatic会从操作数栈中弹出两个元素,并将它们传递给 JVM 为add创建的新框架。此时,main 函数的操作数栈为空。
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
在add栈帧中,这些参数存储在局部变量数组中。前两个字节码命令iload_0和iload_1将第 0 个和第 1 个局部变量加载到栈中。接下来,iadd从操作数栈中弹出栈顶两个元素,将它们相加,然后将结果压回栈。最后,ireturn弹出栈顶元素并将其作为方法的返回值传递给调用栈帧,然后丢弃该栈帧。
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: iconst_3
2: invokestatic #2 // Method add:(II)I
5: istore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: iload_1
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
13: return
main 函数的栈现在保存了add 函数的返回值。istore_1函数弹出该返回值并将其设置为索引 1 处变量的值,即result。getstatic函数将java/io/PrintStream类型的静态字段java /lang/System.out压入栈中。iload_1函数将索引 1 处的变量(即 result 的值,此时 result 等于 5)压入栈中。
因此,此时堆栈中保存了两个值:'out' 字段和值 5。现在,invokevirtual即将调用PrintStream.println方法。它会从堆栈中弹出两个元素:第一个元素是对即将调用 println 方法的对象的引用。第二个元素是一个整数参数,将传递给 println 方法,该方法需要一个参数。主方法会在此处打印add的结果。最后,return命令结束该方法。主框架被丢弃,JVM 进程结束。
“一次编写,随处运行”
那么,是什么让 Java 具有平台无关性呢?这一切都在于字节码。
正如我们所见,任何 Java 程序都会编译成标准的 Java 字节码。然后,JVM 在运行时将其转换为特定的机器指令。我们不再需要确保代码与机器兼容。相反,我们的应用程序可以在任何配备 JVM 的设备上运行,JVM 会为我们完成这一切。JVM 的维护者负责提供不同版本的 JVM,以支持不同的机器和操作系统。
这种架构使得任何 Java 程序都能在安装了 JVM 的任何设备上运行。奇迹就此发生。
最后的想法
Java 开发人员无需了解 JVM 的工作原理即可编写出色的应用程序。
然而,深入研究 JVM 架构,学习它的结构,并了解它如何解释你的代码,将有助于你成为一名更优秀的开发者。它还能帮助你不时地解决一些非常复杂的问题 🙂
附言:如果你想深入了解 JVM 以及它与 Java 异常的关系,那就不用再找了!(都在这里。)
作者:Tzofia Shiftan。首次发表于OverOps 博客。
鏂囩珷鏉ユ簮锛�https://dev.to/overopshq/jvm-architecture-101-get-to-know-your-virtual-machine-1bcd