什么是 Java 中的 JVM-Java快速进阶教程
Java 中的 Java 虚拟机程序整个执行过程的核心。
它基本上是一个程序,提供Java程序执行所需的运行时环境。
换句话说,Java虚拟机(JVM)是一种抽象的计算机机器,负责在特定硬件平台上执行Java字节码(一组高度优化的指令)。它也被称为Java运行时系统。
JVM的规范由Sun Microsystem提供,其实现提供了一个运行时环境来执行我们的Java应用程序。JVM实现被称为Java运行时环境(JRE)。
由于 JVM 不依赖于平台,因此,它可用于许多硬件和软件平台。
Java 虚拟机的内部体系结构 (JVM)
让我们了解一下JVM的内部架构,如下图所示。
如上图所示,JVM包含以下主要组件:
- 类加载器子系统
- 运行时数据区域
- 执行引擎
- 本地方法接口
- Java 原生库
运行时数据区域由以下子组件组成,如下所示:
- 方法区
- Java堆
- Java 栈
- PC寄存器
- 本地方法栈
JVM在内部如何工作?
Java 虚拟机执行以下操作以执行程序。它们如下:
a) 将代码加载到内存中。
b) 验证代码。
c) 执行代码
d) 提供运行时环境。
当我们用 Java 制作程序时,.java程序代码被 Java 编译器转换为由字节码指令组成的.class文件。这个 Java 编译器在 JVM 之外。Java 虚拟机一般会执行以下操作,如下所示:
1. 此.class文件被传输到JVM的类加载器子系统,如上图所示。
在 JVM 中,类装入器子系统是执行以下功能的模块或程序:
a) 首先,类加载器子系统将.class文件加载到内存中。
b) 然后字节码验证器验证所有字节码指令是否正确。如果发现任何指令可疑,则立即拒绝进一步的执行过程。
c) 如果字节码指令正确,它会分配必要的内存来执行程序。
在JVM中内存分为 5 个独立的部分,称为运行时数据区域。它包含程序执行期间的数据和结果。这些区域如下:
1. 方法区域:
方法区域是一个内存块,用于存储 Java 程序的类代码、变量代码和方法。这里的方法是指在类中声明的函数。
2. Java堆:
这是创建对象的运行时数据区域。当 JVM 加载一个类时,会立即在其中构建一个方法和一个堆区域。
3. Java栈:
方法代码存储在“方法”区域中。但是在方法执行期间,它需要更多的内存来存储数据和结果。此内存在 Java 栈上分配。
Java 栈是执行 Java 方法的内存区域。在 Java 栈中,将创建一个单独的帧来执行该方法。
每次调用方法时,都会在栈中创建一个新帧。方法调用完成后,将销毁与其关联的帧。
JVM总是创建一个单独的线程(或进程)来执行每个方法。
4. PC寄存器:
PC(程序计数器)寄存器是那些包含当前正在执行的JVM指令的存储器地址的寄存器(内存区域)。
5. 本地方法栈:
Java程序的方法在Java栈上执行。同样,程序或应用程序中使用的本机方法在本地方法栈上执行。
通常,要执行本机方法,需要 Java 本机方法库。这些头文件通过称为本机方法接口的程序定位并连接到 JVM。
执行引擎
执行引擎由两部分组成:解释器和 JIT(实时)编译器。它们将字节码指令转换为机器码,以便处理器可以执行它们。
在Java中,JVM实现同时使用解释器和JIT编译器将字节码转换为机器码。这种技术称为自适应优化器。
通常,任何编程语言(如C / C++,Fortran,COBOL等)都将使用解释器或编译器将源代码流转换为机器代码。
现在,两个主要问题是:为什么两者都需要解释器和JIT编译器,以及两者如何同时工作?
在理解这一点之前,我们将知道什么是 Java 语言中的 JIT 编译器?
什么是Java中的JIT编译器?
Java 中的 JIT 编译器是 JVM 的一部分,用于提高 Java 程序的执行速度。
换句话说,它用于提高程序执行的性能。它有助于减少执行程序所需的时间。
解释器和 JIT 编译器如何在 Java 中同时工作?
为了理解这一点,让我们考虑下面示例代码。假设这些是字节码指令:
print x; print y; Repeat the following execution 10 times by changing the values of i from 1 to 10: print z;
假设在没有Jit编译器帮助下,以下假设场景执行过程如下:
首先Java 解释器从第一条指令开始执行过程。它解释打印 x;转换为机器代码并将其传输到微处理器。
为此,假设 Java 解释器花了 2 纳秒的时间来完成它。微处理器获取它,执行它,并在屏幕上显示 a 的值。
接着,解释器回到内存中,阅读第二条指令打印 y;并需要另外 2 纳秒将其转换为机器代码。然后将机器代码提供给处理器,处理器执行它。
同样,解释器回来读取第 3 条指令,这是一个循环语句 print z;它应该做 10 次。
假设,解释器第一次需要 2 纳秒才能将 print z 转换为机器代码。
将此机器代码提供给处理器后,它返回内存,读取打印 z;第 2 次指令,再需要 2 纳秒才能将此指令转换为机器代码。然后解释器将此机器代码提供给处理器执行。
同样,解释器回来并读取打印 z;并第三次转换它,再花 2 纳秒。像这样,Java 解释器将转换打印 z;指令进入机器代码10次。
因此,解释器总共需要 10 x 2 = 20 纳秒才能完成此循环。此过程既费时又效率低。
这就是 JVM 不将此字节码指令分配给解释器的原因。它将此代码分配给 JIT 编译器。
在这里,术语编译器是指将Java虚拟机的指令集转换为特定处理器的指令集的转换器。
JIT 编译器如何执行循环指令?
让我们了解 Java 中的 JIT 编译器如何执行循环指令。
首先,JIT 编译器读取 print z 指令,然后将其转换为机器代码。为此,假设 JIT 编译器需要 2 纳秒才能完成此任务。
现在,JIT 编译器分配一个内存块,并将此机器代码指令推送到该内存中。为此,假设还需要 2 纳秒才能完成它。因此,JIT 编译器总共花费了 4 纳秒。
接下来,处理器将从内存中获取此机器代码指令并执行 10 次。
现在您可以观察到 JIT 编译器仅花费 4 纳秒即可完成执行,而解释器需要 20 纳秒才能执行相同的循环。
因此,JIT 编译器提高了执行过程的速度。
这里再次出现一个问题,为什么前两条指令不被JVM分配给JIT编译器。
原因很清楚。JIT 编译器转换每条指令需要 4 纳秒,而解释器实际上只需要 2 纳秒来转换每条指令。
当.class文件加载到内存中时,JVM首先尝试确定哪些代码要提供给解释器,哪些代码要提供给JIT编译器,以便性能更好。
提供给 JIT 编译器的代码块在 Java 中也称为热点。Java语言中的这个特性确实提高了程序在运行时的执行时间,特别是当它要多次执行时。
尽管不建议这样做,但也可以关闭 JIT 编译器选项。
因此,Java 解释器和 JIT 编译器同时工作,将字节码指令转换为机器码指令。
JVM中的垃圾回收器有助于清理未使用的内存,以提高程序的效率。