乐趣区

关于java:通过-GraalVM-将-Java-程序编译成本地机器码

前言

2018 年 4 月,Oracle Labs 新公开了一项黑科技:Graal VM。

这是一个在 HotSpot 虚拟机根底上加强而成的跨语言全栈虚拟机,能够作为“任何语言”的运行平台应用。

当初网络上对于 Graal VM 的相干材料并不多,还是要看官网文档。本文旨在简要介绍:

  • 什么是 Graal VM?
  • Graal VM 有什么益处?
  • Graal VM 有什么毛病?
  • Graal VM 的工作原理是什么?
  • 在 macOS 上装置 Graal VM
  • 将基于 Spring Boot 的 Java 应用程序编译成 本地应用程序

思维导图

上面是一张 Graal VM 的简要 思维导图

一篇通俗易懂的文章:GraalVM:微服务时代的 Java。

什么是 Graal VM

Graal VM 被官网称为“Universal VM”和“Polyglot VM”,是一个在 HotSpot 虚拟机根底上加强而成的跨语言全栈虚拟机,口号是“Run Programs Faster Anywhere”。能够在 Graal VM 上运行“任何语言”,这些语言包含:

  • 基于 Java 虚拟机的语言:Java、Scala、Groovy、Kotlin 等;
  • 基于 LLVM 的语言:C、C++、Rust;
  • 其余语言:JavaScript、Ruby、Python 和 R 语言等。

Graal VM 能够无额定开销地混合应用这些编程语言,反对不同语言中混用对方的接口和对象,也可能反对这些语言应用曾经编写好的本地库文件。

Graal VM 的益处

具体可参考官网文档:Why GraalVM?

我认为最重要的个性是 Ahead-of-Time Compilation。Substrate VM 是一个在 Graal VM 0.20 版本里的极小型的运行时环境,包含了独立的异样解决、同步调度、线程治理、内存治理(垃圾收集)和 JNI 拜访等组件。Substrate VM 还蕴含了一个 本地镜像的结构器(Native Image Generator),用户能够通过本地镜像结构器构建基于构建机器的可执行文件。

结构器采纳指针剖析(Points-To Analysis)技术,从用户提供的程序入口登程,搜寻所有可达的代码。在搜寻的同时,它还将执行初始化代码,并在最终生成可执行文件时,将已初始化的堆保留至一个堆快照之中。

Substrate VM 就能够间接从目标程序开始运行,而无须反复进行 Java 虚拟机的初始化过程。但相应地,原理上也决定了 Substrate VM 必须要求目标程序是齐全关闭的,即不能动静加载其余编译期不可知的代码和类库。基于这个假如,Substrate VM 能力摸索整个编译空间,并通过动态剖析推算出所有虚办法调用的指标办法。

使 Java 适应原生

以往单个服务须要 7*24 小时不间断运行,须要单机高可用,此时 Java 服务就很适宜。然而 Java 应用程序都须要运行在上百兆的 JRE 上,在微服务上就并不适合。

同时在微服务中,利用能够随时拆分,每个利用并不需要很大的内存,而是须要疾速启动、随时更新,也可能不须要长时间运行。Java 应用程序原本启动就很慢,同时须要充沛预热才可能获取高性能。

GraalVM 提前编译就提供了一种解决方案,官网给出应用了 GraalVm 后启动工夫可能进步 50 倍,内存有 5 倍的降落。

Graal VM 的毛病

Java 语言在微服务天生就有劣势,这是因为 Java 诞生之初的口号就是“一次编写,到处运行”。这个口号曾经植入 Java 的基因中。如果想扭转这些(真的要拿 Java 的劣势去和别的语言的劣势相比),会有很多艰难:

  • Java 语言的反射机制,使得在编译期生成可执行文件很艰难。因为通过反射机制能够在运行期间动静调用 API 接口,这些在编译期是无奈感知的。除非放弃反射机制,或者在编译时提供配置文件供反射调用。
  • ASM、CGLIB、Javassist 字节码库会在运行时生成、批改字节码,这些也没法通过 AOT 编译成原生代码。比方 Spring 的依赖注入就应用了 CGLIB 加强。Spring 曾经在新版本中适配了 GraalVM,能够敞开 CGLIB。
  • 放弃 HotSpot 虚拟机自身的外部借款,因为在本地镜像中,连 HotSpot 自身都被毁灭了。
  • 启动工夫、内存应用的确有大幅度优化,然而对于长时间运行的大型利用,未必有 HotSpot 的 Java 应用程序速度快。

Graal VM 的工作原理

Graal VM 的根本工作原理是将这些语言的源代码(例如 JavaScript)或源代码编译后的两头格局(例如 LLVM 字节码)通过解释器转换为能被 Graal VM 承受的两头示意(Intermediate Representation,IR),譬如设计一个解释器专门对 LLVM 输入的字节码进行转换来反对 C 和 C ++ 语言,这个过程称为“程序特化”(Specialized,也常称为 Partial Evaluation)。

Graal VM 提供了 Truffle 工具集来疾速构建面向一种新语言的解释器,并用它构建了一个称为 Sulong 的高性能 LLVM 字节码解释器。

在 macOS 上装置 Graal VM

Linux、Windows 等其余平台能够参考 Install GraalVM。因为我应用 macOS,本篇文章介绍如何在 macOS 上装置 Graal VM,基于 OpenJDK 11 的 GraalVM Community Edition。

装置 Graal VM

macOS 上的 GraalVM 社区版是 tar.gz 文件,JDK 的装置目录是:

/Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home

x86 64 位的 macOS 装置步骤如下:

  1. 在 GraalVM Releases repository on GitHub 上找到 `graalvm-ce-java11-darwin-amd64-20.1.0.tar.gz

` 下载。

  1. 解压缩
tar -xvf graalvm-ce-java11-darwin-amd64-20.1.0.tar.gz
  1. 将文件夹挪动到 /Library/Java/JavaVirtualMachines 目录下(须要应用 sudo)。
sudo mv graalvm-ce-java11-20.1.0 /Library/Java/JavaVirtualMachines

检测是否装置胜利,能够运行命令:

/usr/libexec/java_home -V

运行后果为:

Matching Java Virtual Machines (2):
    11.0.7, x86_64:    "GraalVM CE 20.1.0"    /Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home
    1.8.0_201, x86_64:    "Java SE 8"    /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home

/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home
  1. 因为机器上可能存在多个 JDK,须要配置运行环境。

将 GraalVM bin 目录退出 PATH 环境变量。

export PATH=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home/bin:$PATH

设置 JAVA_HOME 环境变量。

export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home

留神:可能须要批改 bashc 配置文件。

装置 GraalVM 组件

通过上述步骤,曾经装置好了 GraalVM 的根底组件,如果须要额定反对 Python、R 等语言,须要应用 gu 组件。

gu install ruby
gu install r
gu install python
gu install wasm

装置 GraalVM Native Image,运行命令:

gu install native-image

装置 LLVM toolchain 组件,运行命令:

gu install llvm-toolchain

将基于 Spring Boot 的 Java 应用程序编译成本地应用程序

能够参考 GitHub 的 spring-boot-graalvm 我的项目,这个我的项目里具体列出了 GraalVM 编译 Spring Boot Java 应用程序可能呈现的所有问题,并比照了 Java 利用启动与编译成本地可执行的 Java 程序。

Spring 与 Graal VM 独特保护的在 Spring Graal Native 我的项目曾经提供了大多数 Spring Boot 组件的配置信息(以及一些须要在代码层面解决的 Patch),咱们只须要简略依赖该工程即可。这样 Graal VM 就能获取编译期的反射、动静代理等配置。咱们只须要简略依赖工程即可。

须要在 pom.xml 中减少依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-graalvm-native</artifactId>
    <version>0.7.1</version>
</dependency>

指定启动类的门路:

<properties>
    <start-class>com.yano.workflow.WorkflowApplication</start-class>
</properties>

配置一个独立的 profile,在编译时通过 native-image-maven-plugin 插件将其编译成本地可执行文件。

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.nativeimage</groupId>
                    <artifactId>native-image-maven-plugin</artifactId>
                    <version>20.1.0</version>
                    <configuration>
                        <buildArgs>-J-Xmx4G -H:+TraceClassInitialization
                            -H:+ReportExceptionStackTraces
                            -Dspring.graal.remove-unused-autoconfig=true
                            -Dspring.graal.remove-yaml-support=true
                        </buildArgs>
                        <imageName>${project.artifactId}</imageName>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

该插件在 Maven 地方仓库不存在,须要指定 pluginRepositories 和 repositories:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
    </pluginRepository>
</pluginRepositories>

Graal VM 不反对 CGLIB,只能应用 JDK 动静代理,所以该当把 Spring 对一般类的 Bean 加强给敞开掉。Spring Boot 的版本要大于等于 2.2,SpringBootApplication 注解上将 proxyBeanMethods 参数设置为 false。

@SpringBootApplication(proxyBeanMethods = false)
public class SpringBootHelloApplication {public static void main(String[] args) {SpringApplication.run(SpringBootHelloApplication.class, args);
    }

}

在命令行通过 maven 打包我的项目:

mvn -Pnative clean package

最终在 target 目录可能看到可执行文件,大略在 50M 左右,相比 fat jar 为 17M。

java -jar target/spring-boot-graal-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
(()\___ | '_ |'_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v2.3.0.M4)

2020-04-30 15:40:21.187  INFO 40149 --- [main] i.j.s.SpringBootHelloApplication         : Starting SpringBootHelloApplication v0.0.1-SNAPSHOT on PikeBook.fritz.box with PID 40149 (/Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/spring-boot-graal-0.0.1-SNAPSHOT.jar started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm)
2020-04-30 15:40:21.190  INFO 40149 --- [main] i.j.s.SpringBootHelloApplication         : No active profile set, falling back to default profiles: default
2020-04-30 15:40:22.280  INFO 40149 --- [main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2020-04-30 15:40:22.288  INFO 40149 --- [main] i.j.s.SpringBootHelloApplication         : Started SpringBootHelloApplication in 1.47 seconds (JVM running for 1.924)

可能通过命令行间接运行程序,启动速度贼快。比照 Hello World web 一般应用程序,启动工夫是 1.47s,占用内存 491 MB

而编译成本地代码的 Spring Boot 程序,启动速度是 0.078s,占用内存 30 MB

./spring-boot-graal

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
(()\___ | '_ |'_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::

2020-05-01 10:25:31.200  INFO 42231 --- [main] i.j.s.SpringBootHelloApplication         : Starting SpringBootHelloApplication on PikeBook.fritz.box with PID 42231 (/Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/native-image/spring-boot-graal started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/native-image)
2020-05-01 10:25:31.200  INFO 42231 --- [main] i.j.s.SpringBootHelloApplication         : No active profile set, falling back to default profiles: default
2020-05-01 10:25:31.241  WARN 42231 --- [main] io.netty.channel.DefaultChannelId        : Failed to find the current process ID from ''; using a random value: 635087100
2020-05-01 10:25:31.245  INFO 42231 --- [main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2020-05-01 10:25:31.245  INFO 42231 --- [main] i.j.s.SpringBootHelloApplication         : Started SpringBootHelloApplication in 0.078 seconds (JVM running for 0.08)

总结

  • 本篇文章次要探讨 GraalVM 和 Java 的关系,GraalVM 上可能运行很多语言,可参考 Why GraalVM。
  • 留神 Graal 的环境变量配置,配置谬误的话,是没法编译的,同时 JDK 11 须要高版本的 maven 版本。
  • Graal VM 和 GraalVM 是一个东东,官网是叫 GraalVM,然而其余中央都是 Graal VM……
  • 为了适应原生,JDK 本身也在演进。
  • GraalVM 编译的 Java 本地利用仅实用于一次性运行、短时间运行的场景,长时间运行还是传统 Java 程序效率高。
  • 本篇文章的 GitHub 地址:LjyYano/Thinking_in_Java_MindMapping

公众号

coding 笔记、点滴记录,当前的文章也会同步到公众号(Coding Insight)中,心愿大家关注 ^_^

代码和思维导图在 GitHub 我的项目中,欢送大家 star!

退出移动版