用GraalVM Native Image彻底改变Java
主要结论
- GraalVM Native Image是一种提前编译技术,可以生成本地平台的可执行文件。
- 原生可执行文件是容器和云部署的理想选择,因为它们体积小,启动速度快,对CPU和内存的需求也大大降低。
- 在无发行版甚至Scratch容器镜像上部署本地可执行文件,以减少体积和提高安全性。
- 通过配置文件引导的优化和G1垃圾收集器,用GraalVM Native Image构建的本地可执行文件可以达到与JVM相同的峰值吞吐量。
- 在Spring Boot、Micronaut、Quarkus、Gluon Substrate等领先的Java框架的支持下,GraalVM Native Image获得了大量的采用。
Java在企业应用中占主导地位。但在云中,Java比一些竞争对手更昂贵。本地编译使云中的Java更便宜。它创建的应用程序启动速度更快,使用的内存更少。
因此,原生编译给所有的Java用户带来了许多问题。本机Java是如何改变开发的?我们什么时候应该改用原生Java?什么时候不应该?我们应该使用什么框架来开发原生Java?本系列文章将为这些问题提供答案。
这篇InfoQ文章是"本地编译提升Java"系列的一部分。你可以通过RSS订阅来接收通知。
自三年前推出以来,GraalVM已经在Java开发领域引起了一场革命。GraalVM讨论最多的功能之一是Native Image,它是基于时间提前(AOT)的编译。它释放了本地应用程序的运行时性能特征,同时保持了熟悉的开发者生产力和Java生态系统的工具。
Java应用程序的传统执行
Java平台最强大和最有趣的部分之一是Java虚拟机(JVM)执行代码的方式,它能实现巨大的峰值性能。
当你第一次运行你的应用程序时,虚拟机会解释代码并收集剖析信息。尽管JVM解释器的性能很好,但它并不像运行编译后的代码那样快。这就是为什么甲骨文的JVM(HotSpot)也包含及时编译器(JIT),它在你的程序执行过程中,将你的应用代码编译为机器代码。因此,如果你的代码 "变暖"--被频繁执行,它就会被C1 JIT编译器编译为机器代码。然后,如果它仍然被频繁地执行并达到一定的阈值,它就会被顶级JIT编译器(C2或Graal编译器)编译。顶级编译器根据关于哪些代码分支最常被执行、循环被执行的频率以及哪些类型被用于多态代码的分析信息进行优化。
有时,编译器会进行投机性优化。例如,JVM可以根据它所收集的剖析信息,产生一个优化的方法的编译版本。然而,由于JVM上的代码执行是动态的--如果它所做的假设在以后变得无效--JVM将进行非优化:它将无视已编译的代码并恢复到解释模式。正是这种灵活性使JVM如此强大:它开始快速执行代码,利用优化编译器来处理频繁执行的代码,并推测应用更积极的优化。
乍一看,这种方法似乎是运行一个应用程序的理想方式。然而,像大多数事情一样,即使这种方法也有成本和权衡;那么,这里的成本和权衡是什么?当JVM执行其操作(如验证代码、加载类、动态编译和收集剖析信息)时,它进行了复杂的计算,需要大量的CPU时间。除了这个成本,JVM还需要相当大的内存来存储剖析信息,并且需要相当多的时间和内存来启动。随着许多公司将应用程序部署到云中,这些成本变得更加重要,因为启动时间和内存直接影响到部署应用程序的成本。那么,有没有一种方法可以减少启动时间和内存的使用,并仍然保持我们都喜欢的Java生产力、库和工具?
答案是 "有",这就是GraalVM Native Image的作用。
GraalVM的胜利
GraalVM始于10年前Oracle实验室的一个研究项目。甲骨文实验室是甲骨文公司的一个研发部门,负责研究编程语言和虚拟机、机器学习和安全、图形处理和其他领域。GraalVM是甲骨文实验室的一个很好的例子--它是基于多年的研究和100多篇发表的学术论文。
该项目的核心是Graal编译器--一个现代的、高度优化的编译器,从零开始创建。由于有许多先进的优化,在许多情况下,它生成的代码比C2编译器更好。其中一个优化是部分转义分析。它通过标量替换删除了堆上不必要的对象分配,在对象没有逃脱编译单元的分支中,Graal编译器确保对象在逃脱的分支中存在于堆中。
这种方法减少了应用程序的内存占用,因为更少的对象存在于堆中。它也减少了CPU的负荷,因为需要的垃圾收集较少。此外,GraalVM中的高级推测通过利用动态运行时反馈产生更快的机器代码。通过推测某些程序部分在执行过程中不会运行,GraalVM编译器可以使代码的效率更高。
你可能会惊讶地发现,Graal编译器大部分是用Java编写的。如果你看一下GraalVM的核心GitHub仓库,你会发现那里90%以上的代码都是用Java编程语言编写的,这再次证明了Java的强大和通用性。
本地图像如何工作
Graal编译器也作为一个超时空(AOT)编译器工作,产生本地可执行文件。鉴于Java的动态特性,这究竟是如何工作的呢?
与JIT模式不同的是,编译和执行是同时进行的,在AOT模式下,编译器在构建时间内执行所有 编译,然后再执行。这里的主要想法是将所有的 "繁重工作"--昂贵的计算--转移到构建时间,这样就可以一次完成,然后在运行时生成的可执行文件快速启动,并且从一开始就准备好了,因为所有东西都是预先计算和预先编译的。
GraalVM的 "本地图像 "工具将Java字节码作为输入,输出一个本地可执行文件。为了做到这一点,该工具在封闭世界的假设下对字节码进行静态分析。在分析过程中,该工具寻找你的应用程序实际使用的所有代码,并消除一切不必要的东西。
这三个关键概念有助于你更好地理解Native Image的生成过程:
- 点对点分析。GraalVM Native Image确定哪些Java类、方法和字段在运行时是可以到达的,只有这些才会被包含在本地可执行文件中。点对点分析从所有入口点开始,通常是应用程序的主方法。分析会反复处理所有可到达的代码路径,直到到达一个固定点,分析结束。这不仅适用于应用程序代码,也适用于库和JDK类--将应用程序打包成一个独立的二进制文件所需的一切。
- 在构建时进行初始化。GraalVM Native Image默认在运行时进行类的初始化,以确保行为正确。但是如果Native Image能够证明某些类的初始化是安全的,它将在构建时初始化它们。这使得运行时的初始化和检查成为不必要的,并提高了性能。
- 堆快照。Native Image中的堆快照是一个非常有趣的概念,应该有自己的文章。在镜像构建过程中,由静态初始化器分配的Java对象,以及所有可触及的对象,都被写入镜像堆。这意味着你的应用程序在预填充堆的情况下启动得更快。
有趣的是,点对点分析使对象在图像堆中可触及,而建立图像堆的快照可以使新的方法在点对点分析中可触及。因此,点对点分析和堆快照是反复进行的,直到达到一个固定点。
/filters:no_upscale()/articles/native-java-graalvm/en/resources/1Native%20Image%20Build%20Process-1648813770066.jpg)
本地图像构建过程
在分析完成后,Graal将所有可达到的代码编译成一个特定平台的本地可执行文件。该可执行文件本身功能齐全,不需要JVM来运行。因此,你会得到一个纤细而快速的本地可执行版本的Java应用程序:它执行完全相同的功能,但只包含必要的代码及其所需的依赖。
但是谁来负责本地可执行文件中的内存管理和线程调度等功能呢?为此,Native Image包含了Substrate VM--一个纤细的VM实现,它提供了运行时组件,如垃圾收集器和线程调度器。就像Graal编译器一样,Substrate VM是用Java编程语言编写的,并由GraalVM Native Image进行AOT编译,成为本地代码
由于AOT编译和堆快照,Native Image为你的Java应用程序提供了全新的性能特征。接下来让我们仔细看看这个问题。
将Java的启动性能提高到新的水平
你可能听说过,由Native Image生成的可执行文件具有很好的启动性能。但这到底是什么意思呢?
即时启动。与在JVM上运行不同,在JVM上,代码首先被验证、解释,然后(经过预热)最终被编译,而本地可执行文件从一开始就带有优化的机器代码。我喜欢使用的另一个术语是即时性能--一个应用程序在执行的最初几毫秒内就准备好执行有意义的工作,没有任何剖析或编译开销。
| JIT | AOT |
|
|
JIT和本地图像模式的启动时间影响
内存效率:本地可执行文件既不需要JVM及其JIT编译基础设施,也不需要用于编译代码的内存、配置文件数据和字节码缓存。它所需要的只是可执行文件和应用程序数据的内存。这里有一个例子。
/filters:no_upscale()/articles/native-java-graalvm/en/resources/1Memory%20and%20CPU%20Usage%20in%20JIT%20and%20Native%20Image%20Modes-2-1648814886189.jpg)
JIT和本地图像模式下的内存和CPU使用情况
上面的图表显示了一个Web服务器在JVM(左)和作为一个本地可执行文件(右)上的运行时行为。茶色的线条显示了内存的使用量。在JIT模式下是200MB,而本地可执行文件是40MB。红线显示了CPU的活动。JVM在前面描述的热身JIT活动中大量使用CPU,而本地可执行文件几乎不使用CPU,因为所有昂贵的编译操作都发生在构建时间。这种快速和节省资源的运行时行为使Native Image成为一个伟大的部署模式,在这种模式下,用更少的时间使用更少的资源,大大降低了成本--一般来说,微服务、无服务器和云工作负载。
包装大小:一个原生可执行文件只包含所需的代码。这就是为什么它比应用程序代码、库和JVM的综合大小小得多。在某些情况下,例如在资源有限的环境中工作,你的应用程序的打包大小可能很重要。UPX等工具甚至可以进一步压缩本地可执行文件。
峰值性能与JVM相同
但是,峰值性能如何呢?当所有的东西都是提前编译的时候,Native Image是如何在运行时优化峰值吞吐量的?
我们正在努力确保Native Image提供良好的峰值性能和快速启动。目前已经有一些方法可以提高本地可执行文件的峰值性能:
- 配置文件指导下的优化:由于Native Image提前优化和编译代码,在默认情况下,它无法获得运行时的剖析信息来优化应用程序运行时的代码。解决这个问题的方法之一是配置文件引导的优化(PGO)。通过PGO,开发者可以运行一个应用程序,收集剖析信息,然后将其反馈到本地图像生成过程中。本机图像 "工具使用这些信息,根据你的应用程序的运行时行为,优化生成的可执行文件的性能。PGO在GraalVM Enterprise中可用,它是GraalVM的商业版本,由Oracle提供。
- Native Image中的内存管理:Native Image生成的可执行文件中默认的垃圾收集器是Serial GC,这对具有小堆的微服务来说是最理想的。也有一些额外的GC选项可用。
- 串行GC现在有一个新的策略,使年轻一代的幸存者空间能够减少应用程序运行时的内存占用。自从引入这个策略后,我们测量了典型的微服务工作负载(如Spring Petclinic)的峰值吞吐量改进,最高可达23.22%。
- 另外,你可以使用低延迟的G1垃圾收集器,以获得更好的吞吐量(在GraalVM Enterprise中可用)。G1最适用于较大的堆。
使用PGO和G1 GC,本地可执行文件可以达到与JVM相同的峰值性能。
/filters:no_upscale()/articles/native-java-graalvm/en/resources/1Geomean%20of%20Renaissance%20and%20DaCapo%20Benchmarks-1648814886190.jpg)
文艺复兴时期的Geomean和DaCapo基准测试
有了这些选项,你可以用Native Image最大化你的应用程序的每个性能维度:启动时间、内存效率和峰值吞吐量。
反射、配置和其他Native Image的神话破灭
由于Native Image是一种全新的执行Java应用程序的方式,有几件事需要注意。
你可能听说GraalVM Native Image不支持反射。这并不正确。
Native Image在一个封闭世界的假设下执行静态分析。因此,动态的Java功能,如反射,需要额外的配置才能使构建过程成功。当它对您的Java应用进行静态分析时,Native Image会尝试检测并处理对反射API的调用。然而,一般来说,这种自动分析并不总是足够的,在运行时以反射方式访问的程序元素将必须通过配置来指定。你可以手动创建这个配置,或者利用Native Image追踪代理。该代理在JVM上执行程序时跟踪动态特性的使用,并产生一个配置文件。该文件被Native Image工具用来包括通过反射访问的程序部分。虽然代理对获得初始配置很有用,但我们建议你手动检查并在必要时完成它。
在使用Java Native Interface(JNI)、Dynamic Proxy对象和classpath资源时可能需要类似的配置。你也可以使用同一个跟踪代理来配置所有这些功能的使用。
最后,你可以使用GraalVM Dashboard,这是一个基于Web的应用程序,可以可视化Native Image编译,发现哪些包、类和方法被包含在本地可执行文件中,还可以识别哪些对象在堆中占用了最多的空间。
改变Java的云游戏
本机图像对云部署有很大的影响,它可以对你的应用程序的资源消耗情况产生很大的影响。我们已经了解到,由Native Image产生的本地可执行文件启动速度快,需要的内存少。对于云部署来说,这到底意味着什么,GraalVM如何帮助你最小化你的Java容器镜像?
正如我们已经确定的,由Native Image生成的应用程序不需要JVM来运行。它们可以是自成一体的,包括你的应用程序执行所需的一切。这意味着你可以把你的应用程序放到一个纤细的Docker镜像中,它将独立完成全部功能。镜像的大小将取决于你的应用程序做什么,以及它包括哪些依赖性。一个基本的 "Hello, World!"应用程序,用Java微服务框架构建,大约是20MB。
使用Native Image,你还可以构建静态和大部分静态的可执行文件。一个大部分静态的本地可执行文件是针对所有的库静态链接的,除了 "libc",它是由容器镜像提供的。你可以使用一个所谓的无发行版的容器镜像来进行轻量级部署。无发行版镜像只包括运行应用程序的库,而没有外壳、包管理器和其他程序。作为一个例子,你的Docker文件可能只是:
```
FROM gcr.io/distroless/base
COPY build/native-image/application app
ENTRYPOINT ["/app"]
```
对于一个完全自主的部署,甚至不需要容器镜像提供libc,你可以用'musl-libc'静态链接你的应用程序。你可以把它放在一个 "从头开始 "的Docker镜像中,因为它是完全独立的。
在生产中使用本地镜像
到目前为止,我们已经谈到了如何最大限度地提高使用Native Image生成的应用程序的性能,并考虑了一些有用的黑客手段,你可以在构建过程中应用。现在,你还能做些什么来使你的应用程序发挥最大的作用吗?有:很多。
为了简化构建、测试和运行作为本地可执行文件的Java应用程序,请使用GraalVM团队提供的 官方Maven和Gradle插件。此外,这些插件支持本地JUnit 5测试。它们是与JUnit、Micronaut和Spring团队合作开发的,展示了JVM生态系统中合作的一个伟大范例。
要在你的GitHub行动工作流程中设置GraalVM原生镜像,请使用**GitHub行动的GraalVM**。这个可配置的行动支持几个GralVM版本和开发者构建,并完全设置了GralVM和特定组件。
让我们来谈一谈工具的问题。当开发一个你想作为可执行文件发布的Java应用程序时,你可以使用你通常使用的相同工具。 你可以使用任何IDE和任何JDK,包括GraalVM JDK,来构建、测试和调试你的应用程序,然后使用GraalVM Native Image工具来执行最后的本地编译步骤。根据应用程序的复杂程度,Native Image编译可能需要一些时间,所以将其作为最后一步来执行是有意义的。 然而,我们正在开发Native Image的快速开发模式,它将通过不执行生产部署所需的许多优化而大大减少编译时间。
尽管你可以在JVM上开发你的应用程序,然后在开发过程的后期构建一个本地可执行文件,但我们从社区收到了许多关于改善构建时间和资源使用的要求。在过去的几个版本中,我们在这个问题上做了大量的工作。在最新发布的GraalVM(22.0)中,**你可以在大约13.8秒**内从一个hello-world Java应用程序中生成一个本地可执行文件,可执行文件的大小大约为5MB。我们还减少了约10%的内存使用。
要调试使用Native Image构建的可执行文件,你可以从命令行中使用'gdb'(在Linux和macOS上),或者GraalVM的VS Code扩展。本教程提供了分步骤的说明。
要监控本地可执行文件的性能,可以使用JDK Flight Recorder。对Native Image的完全支持仍在进行中,但你已经可以用它来观察自定义和系统事件。
为了进行额外的性能监控,可以生成一个本地可执行文件的堆转储,然后使用VisualVM等工具进行分析。这是GraalVM企业版的一个功能。
被Java框架采用
如果没有Java框架的支持,就很难编写出行业级的应用程序。幸运的是,你不需要这样做。所有主要的框架都支持Native Image(按字母顺序列出)。Gluon Substrate、Helidon、Micronaut、Quarkus和Spring Boot。所有这些框架都利用GraalVM Native Image来大幅提高应用程序的启动时间和资源使用率,使其成为高效云部署的完美选择。本系列的未来文章将介绍各框架如何使用GraalVM Native Image。
Native Image的未来
自首次公开发布以来,Native Image已经取得了巨大的进步。它被Java框架广泛采用,云计算供应商将Native Image作为Java运行时提供,许多库都可以与Native Image开箱即用。我们对开发者的体验做了一些改变,正如我们去年的研究表明,70%使用GraalVM的开发者已经用它来构建和发布本地可执行文件。
我们对Native Image的新功能和改进有很多想法,包括:
- 支持更多的平台
- 简化Java库的配置和兼容性
- 继续改进峰值性能
- 继续与Java框架团队合作,利用所有Native Image的功能,开发新的功能,提高性能,并确保良好的开发者体验
- 引入更快的开发编译模式
- 支持Project Loom的虚拟线程
- IDE支持Native Image的配置和基于代理的配置
- 进一步提高GC性能并增加新的GC实现方式
我们非常感谢社区和我们的合作伙伴帮助我们推动Native Image的发展,使其对每个Java开发者越来越有用。如果您想在Native Image中看到新的功能或改进,请通过GraalVM的社区平台与我们分享您的反馈意见
Java在企业应用中占主导地位。但在云中,Java比一些竞争对手更昂贵。本地编译使云中的Java更便宜。它创建的应用程序启动速度更快,使用的内存更少。
因此,原生编译给所有的Java用户带来了许多问题。本机Java是如何改变开发的?我们什么时候应该改用原生Java?什么时候不应该?我们应该使用什么框架来开发原生Java?本系列文章将为这些问题提供答案。