内容:
- 介绍
- 编译为字节码
- 程序编译与执行示例
- 在虚拟机上执行程序
- 即时 (JIT) 编译
- 结论
一、简介
大家好!今天,我想分享有关运行 Java 编写的应用程序后 JVM(Java 虚拟机)底层发生的情况的知识。如今,有一些流行的开发环境可以帮助您避免考虑 JVM 的内部结构、编译和执行 Java 代码,这可能会导致新开发人员错过这些重要方面。同时,在采访中经常会问到关于这个话题的问题,这就是我决定写一篇文章的原因。
2.编译为字节码
让我们从理论开始。当我们编写任何应用程序时,我们都会创建一个带有扩展名的文件
.java
,并用 Java 编程语言在其中放置代码。这种包含人类可读代码的文件称为
源代码文件。源代码文件准备好后,您需要执行它!但现阶段它包含只有人类才能理解的信息。Java 是一种多平台编程语言。这意味着用 Java 编写的程序可以在任何安装了专用 Java 运行时系统的平台上执行。这个系统称为Java虚拟机(JVM)。为了将程序从源代码翻译成 JVM 可以理解的代码,您需要对其进行编译。JVM 理解的代码称为字节码,包含虚拟机随后将执行的一组指令。
javac
为了将源代码编译为字节码, JDK(Java Development Kit)中包含一个编译器。作为输入,编译器接受一个扩展名为 的文件
.java
,其中包含程序的源代码;作为输出,它生成一个扩展名为 的文件
.class
,其中包含虚拟机执行程序所需的字节码。一旦程序被编译成字节码,就可以使用虚拟机来执行。
3. 程序编译与执行示例
假设我们有一个简单的程序,包含在一个 file 中
Calculator.java
,它接受 2 个数字命令行参数并打印它们相加的结果:
class Calculator {
public static void main(String[] args){
int a = Integer.valueOf(args[0]);
int b = Integer.valueOf(args[1]);
System.out.println(a + b);
}
}
为了将该程序编译为字节码,我们将
javac
在命令行上使用编译器:
javac Calculator.java
编译后,我们收到一个以字节码作为输出的文件
Calculator.class
,我们可以使用安装在我们计算机上的java机器在命令行上使用java命令来执行该文件:
java Calculator 1 2
请注意,在文件名之后,指定了 2 个命令行参数 - 数字 1 和 2。执行程序后,数字 3 将显示在命令行上。在上面的示例中,我们有一个独立的简单类。但是如果该类位于某个包中怎么办?让我们模拟以下情况:创建目录
src/ru/javarush
并将我们的类放置在那里。现在看起来像这样(我们在文件的开头添加了包名称):
package ru.javarush;
class Calculator {
public static void main(String[] args){
int a = Integer.valueOf(args[0]);
int b = Integer.valueOf(args[1]);
System.out.println(a + b);
}
}
让我们使用以下命令编译这样一个类:
javac -d bin src/ru/javarush/Calculator.java
在这个例子中,我们使用了一个额外的编译器选项
-d bin
,将编译后的文件放入一个
bin
结构类似于目录的目录中
src
,但该目录
bin
必须提前创建。此技术用于避免将源代码文件与字节码文件混淆。在运行编译的程序之前,有必要解释一下这个概念
classpath
。
Classpath
是虚拟机查找包和编译类的相对路径。也就是说,通过这种方式我们告诉虚拟机文件系统中哪些目录是Java包层次结构的根。
Classpath
可以在启动程序时使用标志指定
-classpath
。我们使用以下命令启动程序:
java -classpath ./bin ru.javarush.Calculator 1 2
在此示例中,我们需要类的全名,包括它所在的包的名称。最终的文件树如下所示:
├── src
│ └── ru
│ └── javarush
│ └── Calculator.java
└── bin
└── ru
└── javarush
└── Calculator.class
4. 虚拟机执行程序
于是,我们启动了书面程序。但是当虚拟机启动已编译的程序时会发生什么?首先我们先了解一下编译和代码解释的概念是什么意思。
编译是将用高级源语言编写的程序翻译成用类似于机器代码的低级语言编写的等效程序。
解释是对源程序或请求的逐个语句(逐行命令、逐行)分析、处理和立即执行(与编译相反,编译是翻译程序而不执行它)。Java语言同时具有编译器(
javac
)和解释器,解释器是一个虚拟机,它将字节码逐行转换为机器代码并立即执行。这样,当我们运行一个编译好的程序时,虚拟机就开始对其进行解释,即将字节码逐行转换为机器码,并开始执行。不幸的是,纯字节码解释是一个相当长的过程,并且使 java 比其竞争对手慢。为了避免这种情况,引入了一种机制来加速虚拟机对字节码的解释。这种机制称为即时编译(JITC)。
5. 即时(JIT)编译
简单来说,Just-In-Time编译的机制是这样的:如果程序中存在多次执行的代码部分,那么可以将它们一次编译成机器代码,以加快以后的执行速度。将这部分程序编译成机器码后,后续每次调用这部分程序时,虚拟机都会立即执行编译后的机器码,而不是解释它,这自然会加快程序的执行速度。加快程序速度是通过增加内存消耗(我们需要将编译后的机器代码存储在某个地方!)以及增加程序执行期间编译所花费的时间来实现的。JIT 编译是一个相当复杂的机制,所以让我们先简单介绍一下。字节码到机器码的 JIT 编译有 4 个级别。编译级别越高,越复杂,但同时这样的section的执行速度会比级别较低的section更快。JIT - 编译器根据每个程序片段的执行频率来决定为该片段设置什么编译级别。在底层,JVM 使用 2 个 JIT 编译器 - C1 和 C2。C1 编译器也称为客户端编译器,只能编译第 3 级代码。C2 编译器负责第四个、最复杂且最快的编译级别。
从上面我们可以得出结论,对于简单的客户端应用程序,使用 C1 编译器更有利可图,因为在这种情况下,应用程序启动的速度对我们来说很重要。服务器端、长期存在的应用程序可能需要更长的时间来启动,但将来它们必须快速工作并执行其功能 - 这里 C2 编译器适合我们。
当在 x32 版本的 JVM 上运行 Java 程序时,我们可以使用
-client
和标志手动指定要使用的模式
-server
。当指定该标志时,
-client
JVM将不会执行复杂的字节码优化,这将加快应用程序的启动时间并减少内存消耗量。当指定该标志时,
-server
由于复杂的字节码优化,应用程序将需要更长的启动时间,并且将使用更多的内存来存储机器代码,但程序将来会运行得更快。在 x64 版本的 JVM 中,该标志
-client
被忽略,默认情况下使用应用程序服务器配置。
六,结论
我对编译和执行 Java 应用程序如何工作的简要概述到此结束。要点:
-
javac编译器将程序的源代码转换为可以在安装Java虚拟机的任何平台上执行的字节码;
-
编译后,JVM 解释生成的字节码;
-
为了加快 Java 应用程序的速度,JVM 使用即时编译机制,将程序中最常执行的部分转换为机器代码并将其存储在内存中。
我希望本文能够帮助您更深入地了解我们最喜欢的编程语言的工作原理。感谢您的阅读,欢迎批评指正!
GO TO FULL VERSION