Java 虚拟机 (JVM) 的工作原理






4.28/5 (29投票s)
Java 虚拟机的工作原理:JVM 内部。
引言
在 C 和 C++ 等高级编程语言中,我们以人类可读的格式编写程序,然后一个名为编译器的程序将其翻译成计算机能够理解和执行的二进制格式,称为可执行代码。可执行代码依赖于我们用来执行程序的计算机机器;它是机器相关的。在 Java 中,从编写到执行程序的过程非常相似,但有一个重要的区别,这使得我们能够编写与机器无关的 Java 程序。
通过解释器,所有 Java 程序都被编译成一个中间级别,称为字节码。我们可以在任何安装了 Java 运行时环境的计算机上运行编译后的字节码。运行时环境由虚拟机及其支持代码组成。
JVM 是一种仿真
创建 Java 字节码的困难之处在于,源代码是为一台不存在的机器编译的。这台机器被称为 Java 虚拟机,它只存在于我们计算机的内存中。欺骗 Java 编译器为不存在的机器创建字节码只是使 Java 架构中立的巧妙过程的一半。Java 解释器还必须使我们的计算机和字节码文件相信它们正在真实的机器上运行。它通过充当虚拟机和真实机器之间的中介来实现这一点。(见下图。)
Java 虚拟机负责解释 Java 字节码,并将其转换为操作或操作系统调用。例如,建立与远程机器的套接字连接请求将涉及一个操作系统调用。不同的操作系统处理套接字的方式不同——但程序员无需担心这些细节。JVM 的责任是处理这些转换,使得 Java 软件运行的操作系统和 CPU 架构与开发人员完全无关。(见下图。)
Java 虚拟机的基本组成部分
在计算机内存中创建虚拟机需要构建真实计算机的每个主要功能,直到程序运行的环境。这些功能可以分解为七个基本部分
- 一组寄存器
- 一个堆栈
- 一个执行环境
- 一个垃圾回收堆
- 一个常量池
- 一个方法存储区
- 一个指令集
寄存器
Java 虚拟机的寄存器与我们计算机中的寄存器类似。然而,由于虚拟机是基于堆栈的,其寄存器不用于传递或接收参数。在 Java 中,寄存器保存机器的状态,并在每行字节码执行后更新以维护该状态。以下四个寄存器保存虚拟机的状态
- frame,引用帧,包含指向当前方法执行环境的指针。
- optop,操作数顶部,包含指向操作数堆栈顶部的指针,用于评估算术表达式。
- pc,程序计数器,包含要执行的下一条字节码的地址。
- vars,变量寄存器,包含指向局部变量的指针。
所有这些寄存器都是 32 位宽,并立即分配。这之所以可能,是因为编译器知道局部变量和操作数堆栈的大小,并且解释器知道执行环境的大小。
技术栈
Java 虚拟机使用操作数堆栈向方法和操作提供参数,并从它们接收结果。所有字节码指令都从堆栈中取出操作数,对其进行操作,并将结果返回到堆栈。与虚拟机中的寄存器一样,操作数堆栈是 32 位宽的。
操作数堆栈遵循后进先出(LIFO)方法,并期望堆栈上的操作数按特定顺序排列。例如,isub
字节码指令期望两个整数存储在堆栈顶部,这意味着操作数必须已由前一组指令压入。isub
弹出堆栈中的操作数,对其进行减法运算,然后将结果压回堆栈。
在 Java 中,整数是一种基本数据类型。每种基本数据类型都有独特的指令,告诉它如何操作该类型的操作数。例如,lsub
字节码用于执行长整型减法,fsub
字节码用于执行浮点减法,而 dsub
字节码用于执行长整型减法。因此,将两个整数压入堆栈然后将它们视为单个长整数是违法的。然而,将一个 64 位长整数压入堆栈并使其占用两个 32 位槽是合法的。
我们的 Java 程序中的每个方法都与一个堆栈帧相关联。堆栈帧包含方法的状态,其中包含三组数据:方法的局部变量、方法的执行环境和方法的操作数堆栈。尽管局部变量和执行环境数据集的大小在方法调用开始时总是固定的,但操作数堆栈的大小会随着方法字节码指令的执行而改变。由于 Java 堆栈是 32 位宽的,因此不能保证 64 位数字是 64 位对齐的。
执行环境
执行环境作为数据集维护在堆栈中,用于处理动态链接、正常方法返回和异常生成。为了处理动态链接,执行环境包含对当前方法和当前类的符号引用以及变量。这些符号调用通过动态链接到符号表来转换为实际的方法调用。
每当方法正常完成时,一个值会返回给调用方法。执行环境通过恢复调用者的寄存器并增加调用者的程序计数器以跳过方法调用指令来处理正常方法返回。程序的执行随后在调用方法的执行环境中继续。
如果当前方法的执行正常完成,则一个值将返回给调用方法。当调用方法执行适合返回类型的返回指令时,就会发生这种情况。
如果调用方法执行的返回指令不适合返回类型,则该方法会抛出异常或错误。可能发生的错误包括动态链接失败(例如找不到类文件)或运行时错误(例如数组边界外的引用)。当发生错误时,执行环境会生成异常。
垃圾回收堆
每个在 Java 运行时环境中运行的程序都有一个垃圾回收堆分配给它。由于类对象的实例是从这个堆中分配的,因此堆的另一个名称是内存分配池。默认情况下,大多数系统上的堆大小设置为 1MB。
尽管我们在启动程序时将堆设置为特定大小,但它可能会增长,例如,当分配新对象时。为了确保堆不会变得太大,不再使用的对象会自动由 Java 虚拟机解除分配或进行垃圾回收。
Java 以后台线程的形式执行自动垃圾回收。在 Java 运行时环境中运行的每个线程都与两个堆栈相关联:第一个堆栈用于 Java 代码;第二个堆栈用于 C 代码。这些堆栈使用的内存来自总系统内存池。每当一个新线程开始执行时,都会为其 Java 代码和 C 代码分配最大堆栈大小。默认情况下,在大多数系统上,Java 代码堆栈的最大大小为 400KB,C 代码堆栈的最大大小为 128KB。
如果我们的系统有内存限制,我们可以强制 Java 执行更积极的清理,从而减少使用的总内存量。为此,请减少 Java 和 C 代码堆栈的最大大小。如果我们的系统内存充足,我们可以强制 Java 执行较不积极的清理,从而减少后台处理量。为此,请增加 Java 和 C 代码堆栈的最大大小。
常量池
堆中的每个类都关联着一个常量池。由于常量不会改变,它们通常在编译时创建。常量池中的项编码了特定类中任何方法使用的所有名称。该类包含常量的数量计数,以及一个偏移量,指定特定常量列表在类描述中开始的位置。
与常量关联的所有信息都遵循基于常量类型的特定格式。例如,类级别常量用于表示类或接口,并具有以下格式
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
其中 tag
是 CONSTANT_Class
的值,name_index
提供类的 string
名称。int[][]
的类名为 [[I
。Thread[]
的类名为 [Ljava.lang.Thread;
。
方法区
Java 的方法区类似于其他编程语言使用的运行时环境的编译代码区。它存储与编译代码中的方法关联的字节码指令,以及执行环境进行动态链接所需的符号表。任何可能需要与方法关联的调试或附加信息也存储在此区域中。
字节码指令集
尽管程序员更喜欢用高级格式编写代码,但我们的计算机无法直接执行此代码,这就是为什么我们必须在运行 Java 程序之前对其进行编译。通常,编译后的代码要么是机器可读的格式,称为机器语言,要么是中间级别的格式,例如汇编语言或 Java 字节码。
Java 虚拟机使用的字节码指令类似于汇编指令。如果你曾使用过汇编语言,你会知道指令集为了效率而精简到最小,并且诸如打印到屏幕之类的任务是通过一系列指令完成的。例如,Java 语言允许我们使用一行代码打印到屏幕,例如
System.out.println("Hello world!");
在编译时,Java 编译器将单行打印语句转换为以下字节码
0 getstatic #6 <Field java.lang.System.out Ljava/io/PrintStream;>
3 ldc #1 <String "Hello world!">
5 invokevirtual #7 <Method java.io.PrintStream.println(Ljava/lang/String;)V>
8 return
JDK 提供了一个用于检查字节码的工具,称为 Java 类文件反汇编程序。我们可以通过在命令行键入 javap 来运行反汇编程序。
由于字节码指令的格式如此之低级,我们的程序执行速度几乎与编译为机器语言的程序相同。机器语言中的所有指令都由 0 和 1 的字节流表示。在低级语言中,0 和 1 的字节流被合适的助记符取代,例如字节码指令 isub
。与汇编语言一样,字节码指令的基本格式是
<operation> <operands(s)>
因此,字节码指令集中的一条指令由一个 1 字节的操作码(指定要执行的操作)和零个或多个操作数(提供操作将使用的参数或数据)组成。
摘要
Java 虚拟机仅存在于我们计算机的内存中。在计算机内存中复制一台机器需要七个关键对象:一组寄存器、一个堆栈、一个执行环境、一个垃圾回收堆、一个常量池、一个方法存储区,以及一个将它们全部连接起来的机制。这个机制就是字节码指令集。
要检查字节码,我们可以使用 Java 类文件反汇编器 javap。通过详细检查字节码指令,我们可以深入了解 Java 虚拟机和 Java 本身的内部工作原理。每个字节码指令都执行一个范围极其有限的特定功能,例如将对象压入堆栈或将对象从堆栈中弹出。这些基本功能的组合代表了 Java 编程语言中定义为语句的复杂高级任务。令人惊奇的是,有时几十个字节码指令用于执行单个 Java 语句指定的操作。当我们将这些字节码指令与虚拟机的七个关键对象结合使用时,Java 获得了平台独立性,并成为世界上最强大和多功能的编程语言。