大家好,我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。

文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。

今天说一说 GraalVM。

GraalVM 是 Oracle 大力发展和想要推广的新一代 JVM ,目前很多框架都已经渐渐支持 GraalVM 了,比如我们在用的 Spring 也已经推出了对 GraalVM 兼容的工具包了。

既然说的这么厉害,那么它到底是何方神圣呢。

GraalVM 和 JVM 的关系

既然叫做VM,那肯定和 JVM 有关系的吧。JVM 全称 Java 虚拟机,我们都知道,Java 程序是运行在虚拟机上的,虚拟机提供 Java 运行时,支持解释执行和部分的(JIT)即时编译器,并且负责分配和管理 Java 运行所需的内存,我们所说的各种垃圾收集器都工作在 JVM 中。

比如 Oracle JDK、OpenJDK ,默认的 JVM 是 HotSpot 虚拟机,这是当前应用最广泛的一个虚拟机。我们平时见到的各种将虚拟机的书籍、文章、面试题,基本上都是说的 HotSpot 虚拟机。

除此之外,还有一些商用,或者说小众的虚拟机存在,比如IBM 的J9 JVM,商用的 Zing VM 等。

那 GraalVM 是另一种 Java 虚拟机吗?

是,又不全是。

GraalVM 可以完全取代上面提到的那几种虚拟机,比如 HotSpot。把你之前运行在 HotSpot 上的代码直接平移到 GraalVM 上,不用做任何的改变,甚至都感知不到,项目可以完美的运行。

但是 GraalVM 还有更广泛的用途,不仅支持 Java 语言,还支持其他语言。这些其他语言不仅包括嫡系的 JVM 系语言,例如 Kotlin、Scala,还包括例如 JavaScript、Nodejs、Ruby、Python 等。

GraalVM 的野心不止于此,看上面的图,它的目的是搭建一个 Framework,最终的目标是想要支持任何一种语言,无论哪种语言,可以共同跑在 GraalVM 上,不存在跨语言调用的壁垒。

GraalVM 和JDK有什么关系

Java 虚拟机都是内置在 JDK 中的,比如Orcale JDK、OpenJDK,默认内置的都是 HotSpot 虚拟机。

GraalVM 也是一种 JDK,一种高性能的 JDK。完全可以用它替代 OpenJDK、Orcale JDK。

GraalVM 如何运行 Java 程序

说了半天,是不是还是不知道 GraalVM 到底是什么。

  • GraalVM - 还包含 Graal (JIT)即时编译器,可以结合 HotSpot 使用

  • GraalVM – 是一种高性能 JDK,旨在加速 Java 应用程序性能,同时消耗更少的资源。

  • GraalVM - 是一种支持多语言混编的虚拟机程序,不仅可以运行 JVM 系列的语言,也可支持其他语言。

GraalVM 提供了两种方式来运行 Java 程序。

第一种:结合 HotSpot 使用

上面说了,GraalVM 包含 Graal (JIT)即时编译器,自从 JDK 9u 版本之后,Orcale JDK 和 OpenJDK 就集成了 Graal 即时编译器。我们知道 Java 既有解释运行也有即时编译。

当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。比如多次执行的方法或者循环、递归等。

JDK 默认使用的是 C2 即时编译器,C2是用C++编写的。而使用下面的参数可以用 Graal 替换 C2。

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

Graal 编译器是用 Java 实现的,用 Java 实现自己的编译器。Graal 基于一些假设的条件,采取更加激进的方式进行优化。采用 Graal 编译器之后,对性能有会有一定的提升。

但是如果你还是在用 JDK8,那对不起了,GraalVM 的一切都用不了。

第二种:AOT 编译本地可执行程序

这是 GraalVM 真正厉害的地方。

AOT 提前编译,是相对于即时编译而言的。AOT在运行过程中耗费 CPU 资源来进行即时编译,而程序也能够在启动的瞬间就达到理想的性能。例如 C 和 C++语言采用的是AOT静态编译,直接将代码转换成机器码执行。而 Java 一直采用的是解释 + 即时编译技术,大多数情况下 Java 即时编译的性能并不比静态编译差,但是还是一直朝着 AOT 编译的方向努力。

但是 Java 对于 AOT 来说有一些难点,比如类的动态加载和反射调用。

GraalVM 显然是已经克服了这些问题,使用 GraalVM 可以直接将 Java 代码编译成本地机器码形态的可执行程序。

我们目前运行 Java 一定要安装 JDK 或者 JRE 对不对,如果将程序直接编译成可执行程序,就不用在服务器上安装 JDK 或 JRE 了。那就是说运行 Java 代码其实也可以不用虚拟机了是吗?

GraalVM 的 AOT 编译实际上是借助了 SubstrateVM 编译框架,可以将 SubstrateVM 理解为一个内嵌精简版的 JVM,包含异常处理,同步,线程管理,内存管理(垃圾回收)和 JNI 等组件。

SubstrateVM 的启动时间非常短,内存开销非常少。用这种方式编译出的 Java 程序的执行时间可与C语言持平。

下图是使用即时编译(JVM运行)与 AOT (原生可执行程序)两种方式的 CPU 和内存使用情况对比,可以看出来,AOT 方式下 CPU 和内存的使用都非常少。

除了运行时占用的内存少之外,用这种方式最终生成的可执行文件也非常小。这对于云端部署非常友好。目前很多场景下都使用 Docker 容器的方式部署,打一个 Java 程序的镜像包要包含完整的 JVM 环境和编译好的 Jar 包。而AOT 方式可以最大限度的缩小 Docker 镜像的体积。

缺点

好处多多,当然也有一些弊端。对于反射这种纯粹在运行时才能确定的部分,不可能完全通过优化编译器解决,只能通过增加配置的方式解决。麻烦是麻烦了一点,但是是可行的,Spring Boot 2.7的版本已经支持原生镜像了,Spring 这种非常依赖反射的框架都可以支撑,我们用起来也应该没问题。

GraalVM 如何支持多语言

要支持多语言,就要说到 GraalVM 中的另一个核心组件 Truffle 了。

Truffle 是一个用 Java 写就的语言实现框架。基于 Truffle 的语言实现仅需用 Java 实现词法分析、语法分析以及针对语法分析所生成的抽象语法树(Abstract Syntax Tree,AST)的解释执行器,便可以享用由 Truffle 提供的各项运行时优化。

就一个完整的 Truffle 语言实现而言,由于实现本身以及其所依赖的 Truffle 框架部分都是用 Java 实现的,因此它可以运行在任何 Java 虚拟机之上。

当然,如果 Truffle 运行在附带了 Graal 编译器的 Java 虚拟机之上,那么它将调用 Graal 编译器所提供的 API,主动触发对 Truffle 语言的即时编译,将对 AST 的解释执行转换为执行即时编译后的机器码。

目前除了 Java, JavaScript、Ruby、Python 和许多其他流行语言都已经可以运行在 GraalVM 之上了。

GraalVM 官方还提供了完整的文档,当有一天你开发了一款新的语言,也可以用 Truffle 让它跑在 GraalVM 上。

安装和使用

GraalVm 目前的最新版本是 22.3,分为社区版和企业版,就好像 OpenJDK 和 商用的 Orcale 的 JDK ,企业版会多一些性能分析的功能,用来帮助更大程度的优化性能。

社区版是基于OpenJDK 11.0.17, 17.0.5, 19.0.1,而商业版基于Oracle JDK 8u351, 11.0.17, 17.0.5, 19.0.1,所以,如果你想用免费的,只能将程序升级到 JDK 11 以上了。

GraalVM 支持 Windows、Linux、MacOS ,可以用命令安装最新版,或者直接下载对应 Java 版本的。

我是下载的 Java 11 的版本,下载下来的压缩包,直接解压,然后配置环境变量。把解压目录配置到环境变量的 JAVA_HOME就可以了。

解压好其实就相当于安装完毕了,查看一下版本。

进入到解压目录下的bin目录中,运行 java -version。运行结果如下:

运行代码

常用方式运行

也就是我们平时一直在用的这种方式,把 GrralVM 当做 OpenJDK 使用,只不过把即时编译器换成了 Graal 。就是前面说的第一种方式。

安装完成后,就可以把它当做正常的 JDK 使用了,比如 javacjpsjmap等都可以直接用了。大多数人还是用 IDEA 的,所以就直接在 IDEA 中使用就好了。

1、先随意创建一个 Java 项目。

2、创建完成后,打开项目设置。

3、在打开的项目设置弹出框中选择 SDKs,点击加号,选择前面解压的 GraalVM 目录。

4、然后选择刚刚添加的这个 JDK。

5、最后运行一段测试代码。

public class HelloWorld {
public static void main(String[] args) throws Exception {
System.out.println("Hello GraalVM!");
Thread.sleep(1000 * 100 * 100);
}
}

上面这样的运行方式,其实就相当于前面说的第一种运行方式

native-image 方式运行

这种方式就是 AOT 编译成机器码,已可执行文件的形式出现。native-image 可以命令行的形式执行,也可以在配合 Maven 执行,我这儿就直接演示用 Maven 形式的了,毕竟IDEA 搭配 Maven 用习惯了。

1、安装native-image 工具包

native-image 是用来进行 AOT 编译打包的工具,先把这个装上,才能进行后面的步骤。

安装好 GraalVM 后,在 bin目录下有一个叫做 gu的工具,用这个工具安装,如果将 bin目录添加到环境中,直接下面的命令安装就行了。

gu install native-image

如果没有将 bin目录加到环境变量中,要进入到 bin目录下,执行下面的命令安装。

./gu install native-image

这个过程可能比较慢,因为要去 github 上下载东西,如果一次没成功(比如超时),多试两次就好了。

2、配置 Maven

配置各种版本

 <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.specification.version} </maven.compiler.source>
<maven.compiler.target>${java.specification.version}</maven.compiler.target>
<native.maven.plugin.version>0.9.12</native.maven.plugin.version>
<imageName>graalvm-demo-image</imageName>
<mainClass>org.graalvm.HelloWorld</mainClass>
</properties>

native.maven.plugin.version是要用到的编译为可执行程序的 Maven 插件版本。

imageName是生成的可执行程序的名称。

mainClass是入口类全名称。

配置 build 插件

  <build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>java-agent</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>java</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>${mainClass}</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>native</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${project.build.directory}/${imageName}</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.source}</target>
</configuration>
</plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
</configuration>
</plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin> </plugins> </build>

配置 profiles

  <profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<configuration>
<fallback>false</fallback>
<buildArgs>
<arg>-H:DashboardDump=fortune -H:+DashboardAll</arg>
</buildArgs>
<agent>
<enabled>true</enabled>
<options>
<option>experimental-class-loader-support</option>
</options>
</agent>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>

3、使用 maven 编译,打包成本地可执行程序。

执行 Maven 命令

mvn clean package

或者

mvn  -Pnative -Dagent package

编译打包的过程比较慢,因为要直接编译成机器码,所以比一般的编译过程要慢一些。看到下面的输入日志,说明打包成功了。

4、运行可执行程序包,打开 target 目录,已经看到了graalvm-demo-image可执行程序包了,大小为 11.58M。

然后就可以运行它了,进入到目录下,执行下面的命令运行,可以看到正常输出了。注意了,这时候已经是没有用到本地 JVM 了。

./graalvm-demo-image
Hello GraalVM!

这时候,用 jps -l命令已经看不到这个进程了,只能通过 ps看了。

总结

虽然我们还没有看到有哪个公司说在用 GraalVM 了,但是 QuarkusSpring BootSpring等很多的框架都已经支持 GraalVM 的 Native-image 模式,而且在 Orcale 的大力推广下,相信不久之后就会出现在更多的产品中。赶紧体验一下吧。


如果觉得还不错的话,给个推荐吧!

公众号「古时的风筝」,Java 开发者,专注 Java 及周边生态。坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!

过两年 JVM 可能就要被 GraalVM 替代了的更多相关文章

  1. 两道JVM面试题,竟让我回忆起了中学时代!

    作者:肥朝 原文链接:https://mp.weixin.qq.com/s/4wJ6ANal0blLOseasfIuVw 中学授课模式 考虑到可能有部分粉丝对JVM参数不清楚,所以我们参照中学的授课模 ...

  2. 整理两个JVM博客集合,空闲时候可以看

    纯洁的微笑写的:https://www.cnblogs.com/ityouknow/p/5614961.html 集合:http://www.cnblogs.com/ityouknow/categor ...

  3. 深入理解JVM(6)——JVM性能调优实战

    如何在高性能服务器上进行JVM调优:以便充分利用高性能服务器的硬件资源,有两种JVM调优方案. 一.        采用64位操作系统,并为JVM分配大内存 分析:如果JVM中堆内存太小,那么就会频繁 ...

  4. JVM内存区域划分及垃圾回收

    第一部分.闲扯+概述 近来在研读<深入理解java虚拟机>一书,读完之后做个小结,算是记录一下自己的学习所得,在成长的路上,只能死磕. 要理解JVM,就要先从其内存区域划分开始,知道其由几 ...

  5. 深入理解JVM虚拟机3:垃圾回收器详解

    JVM GC基本原理与GC算法 Java的内存分配与回收全部由JVM垃圾回收进程自动完成.与C语言不同,Java开发者不需要自己编写代码实现垃圾回收.这是Java深受大家欢迎的众多特性之一,能够帮助程 ...

  6. Java虚拟机系列一:一文搞懂 JVM 架构和运行时数据区

    前言 之前写博客一直比较随性,主题也很随意,就是想到什么写什么,对什么感兴趣就写什么.虽然写起来无拘无束,自在随意,但也带来了一些问题,每次写完一篇后就要去纠结下一篇到底写什么,看来选择太多也不是好事 ...

  7. Java性能优化权威指南-读书笔记(五)-JVM性能调优-吞吐量

    吞吐量是指,应用程序的TPS: 每秒多少次事务,QPS: 每秒多少次查询等性能指标. 吞吐量调优就是减少垃圾收集器消耗的CPU周期数,从而将更多的CPU周期用于执行应用程序. CMS吞吐调优 CMS包 ...

  8. JVM的粗略简述

    什么是Java虚拟机 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机有自己完善的硬体架构,如处理器.堆栈.寄存器等,还具有相应的指令系统.JVM屏蔽了与 ...

  9. 关于JVM的类型和模式

    原文出处: 摆渡者 引言 曾几何时,我也敲打过无数次这样的命令: 然而之前的我都只关心过版本号,也就是第一行的内容.今天,我们就来看看第3行输出的内容:JVM的类型和工作模式. 其实说Server和C ...

  10. JVM启动过程——JVM之一

    JVM是Java程序运行的环境,同时是一个操作系统的一个应用程序进程,因此它有自己的生命周期,也有自己的代码和数据空间. JVM体系主要是两个JVM的内部体系结构分为三个子系统和两大组件,分别是:类装 ...

随机推荐

  1. Ubuntu20.04和Docker环境下安装Redash中文版

    创建Ubunt20.04虚拟机,请参考:https://www.linuxidc.com/Linux/2020-03/162547.htm 一.安装基础环境: # 1.更换APT国内源 sudo se ...

  2. nsis离开自定义页面保存设置

    这是群里一位朋友问他的自定义页面设置完成后返回上一步无法保存怎么办写的一个小例子,拓展了下,只要不关闭,不管上一步还是进入下一步返回都可以保留原页面设置. !include LogicLib.nsh ...

  3. python流程控制下-for、while循环补充

    循环结构之for循环 实现循环结构还可以用关键字for. for关键字 我们来看这一段代码: emotions = ['smile', 'laugh', 'cry', 'angry'] for emo ...

  4. 记一个深层的bug

    1. 业务场景 产品需要每隔几天进行一次组件的更新,在自动化测试中,每隔30s检测一次更新源上的某个文件MD5值是否与本地一致,不一致代表有更新的版本,开始更新. 2. 问题出现 一个再平常不过的繁忙 ...

  5. 从0搭建vue3组件库: 如何完整搭建一个前端脚手架?

    相信大家在前端开发中都使用过很多前端脚手架,如vue-cli,create-vite,create-vue等:本篇文章将会为大家详细介绍这些前端脚手架是如何实现的,并且从零实现一个create-kit ...

  6. vue3+element-plus+登录逻辑token+环境搭建

    vue3+element-plus+登录逻辑token环境搭建 安装脚手架工具 1 npm i @vue/cli@4.5.13 -g 验证是否安装成功 1 vue -V # 输出 @vue/cli 4 ...

  7. 论文笔记 - Noisy Channel Language Model Prompting for Few-Shot Text Classification

    Direct && Noise Channel 进一步把语言模型推理的模式分为了: 直推模式(Direct): 噪声通道模式(Noise channel). 直观来看: Direct ...

  8. Python的几种lambda排序方法

    1.对单个变量进行排序 #lst = [[5,8],[5,3],[3,1]] lst.sort(key = lambda x : x[1]) #lst = [[3,1],[5,8],[5,3]] 以元 ...

  9. 嵌入式-C语言基础:理解形参和实参的区别

    #include<stdio.h> //实参:函数原型中声明函数后面带的参数 int test(int x)//函数原型 { //函数体 printf("test里面的x地址=% ...

  10. C#多线程之线程高级(下)

    四.Monitor信号构造 信号构造本质:一个线程阻塞直到收到另一个线程发来的通知. 当多线程Wait同一对象时,就形成了一个"等待队列(waiting queue)",和用于等待 ...