对于每一位 Javer
而言,想必对于 JVM
都并不陌生,即使在实际开发中并没有深入研究,但或多或少对其仍有一定的了解。
所谓 JVM
缩写于 Java Virtual Machine
即 Java
虚拟机,所以开发的程序都是运行于此虚拟机之前,也是 Java
引以为傲的特性即一次编译任意运行,作为应用程序于操作系统之间沟通的桥梁,只需安装了 JRE
环境即可跨平台运行。
一、JIT编译
1. 基础介绍
若提到 JVM
,那必然绕不开 JIT(Just In Time)
编译,故名思意即时编译。
我们都知道开发的应用程序想要运行在 JVM
之上,需要先将 .java
文件编译为 .class
字节文件。当编译完成之后,程序的字节文件则可在任意的 JVM
环境上运行,由 JVM
负责解析字节文件交由操作系统执行。
让我们将目光聚焦到 JVM
与操作系统交互上,操作系统并不认识编译之后的 .class
文件,需要由 JVM
承担其转译的工作,但通过此方式虽达到了效果但性能却并不令人满意。
那有什么方式能够解决呢?最简单的方式即将编译后的字节文件再次转译为操作系统可识别的底层汇编机器码,则操作系统可直接进行执行,省去了 JVM
解释的这一动作,而这个过程即称为 JIT
编译。
2. 编译类型
简单来讲,JIT
的工作即将字节文件转化为操作系统可直接执行的机器码。
在之前介绍垃圾回收器的时候提到过程序的启动支持 Client
与 Server
两种模式,而同样 JIT
对应的也有 C1
与 C2
模式。
二者的区别在于 Server
模式即 C2
相对于 C1
而言在解析编译为机器码时做出更多的编译优化,对于长期运行于服务器上的引用而言相对更为合适。
3. 编译优化
但在具体的场景中相对更为复杂,两种编译模式更多的是搭配进行。
当我们将编写程序编译为字节文件时,此时编译执行的策略是 C1
模式,即与实际编写的代码并无差异。而在 JVM
实时运行过程中,当 JVM
检测到某一代码块的执行频率提高时,则会动态基于 C2
模式实时调整优化,这也是即时编译名称的由来。
对于 JIT
编译的优化可谓门道颇深,在 《Effective Java》
第 66
节中也提到 JIT
优化中的一种 hoisting
即优化提升,感兴趣的可自行查看原文。
这里举个示例进行演示:
public static void main(String[] args) {
for(int i = 1; i < 10*100; i++) {
System.out.println(i);
}
}
上述示例中循环执行了 1000
次打印输出,按照直觉而言 for
循环结束判断的表达式 10*100
每次循环都执行计算一次,但实际上并非如此。
正是由于 JIT
编译优化的存在的,实际运行生效的结果将为下述代码,即 hoisting
优化提升会将计算前置,从而整个循环过程计算只会执行一次。
想要了解更多的推荐去看周志明老师出版的 《深入理解 Java 虚拟机》
,在第 11
章详细介绍了 JIT
内容。
public static void main(String[] args) {
int count = 10*100;
for(int i = 1; i < count; i++) {
System.out.println(i);
}
}
二、AOT编译
1. 预编译
那讲了这么多 JIT
究竟和 GraalVM
又有什么关联呢?
Java
诞生至今已发展数十年,且随着技术的不断演进想要在原有的 JIT
基础之上提出更多的特性以及优化所需要付出成本是十分高昂的,那最简单的方式就是推到重来。这也是 GraalVM
所诞生的一大原有,且不同于 JVM
的由 C++
实现,GraalVM
实现了自举即通过 Java
语言开发实现,同时引入新的虚拟机接口规范 JVMCI(JVM Compiler Interface)
。
在 GraalVM
中一大亮点即提出了预编译 AOT(Ahead-Of-Time)
,它不像 JIT
中的 C2
一样为运行时实时动态调整,AOT
在编译构建时即会对代码进行分析优化后编译,程序运行时则不再动态调整。
虽然 AOT
仍然达不到 JIT
的执行效率,但万物皆有取舍。在内存管理方面,AOT
由于预编译的特性无需动态分析调整,节省了方法堆栈等消耗,程序运行内存的消耗相对于 JVM
中的 JIT
取得了明显了降低。
2. 原生镜像
虽然 GraalVM
中引入了 AOT
,不过在默认的运行模式下仍是基于 JIT
方法实现,值得一提的是 GraalVM
中的 JIT
同样基于 Java
重新设计开发。
因此,想要使用 AOT
则需要将 Jar
文件编译为 Native Images
。所谓 Native Images
即将程序直接构建为镜像容器文件,与传统的可执行文件不同,此时 Native Images
运行不再依赖于 JVM
虚拟机可独立运行。
Native Images
的构建方式也并不复杂,在安装完 GraalVM
并配置系统环境变量之后,native-image
便会可生效,可通过下述命令查看版本验证是否可用。
native-image --version
当然想要编译为可执行二进制文件需要安装相应的环境依赖,以 Windows
环境为例则需要安装 Vistual Studio
,官网教程描述的已十分详细,这里就不再介绍,链接直达。
Windows
下安装Vistual Studio
时需要注意,GraalVM
中读取目录为默认安装路径,若安装Vistual Studio
时修改了默认路径,则需要通过mklink /d
命令为两个目录创建软链接。详细内容可参考
Issue
: Error: Failed to find ‘vcvarsall.bat’ in a Visual Studio installation.
环境准备完成之后即可通过下述命令编译 Jar
包为 Native Images
,命令执行后会生成同名的 exe
文件。
native-image -jar <target-file>.jar
当然你也可以选择在 Maven
工程 pom.xml
文件添加下述内容后执行 mvn clean package -Pnative
命令,完成后将在工程 target
目录下生成 xxx.exe
可执行文件。
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.4</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
3. 反射信息
由于 Native Image
是静态编译的,任何在运行时使用的反射、动态代理等特性都需要显式声明,否则 GraalVM
编译器无法知道它们的存在。
通过下述方式使用 native-image-agent
来自动生成反射和动态代理的配置文件:
java -agentlib:native-image-agent=config-output-dir=./configs -jar <target-file>.jar
运行后 configs
目录中会生成配置文件如 reflection-config.json
和 proxy-config.json
,可以将这些配置文件一起包含在原生镜像构建过程中。
在构建可以使用 -H:ConfigurationFileDirectories=./configs
选项将这些配置文件包含到构建过程中:
native-image -jar <target-file>.jar -H:ConfigurationFileDirectories=./configs
参考链接