Java 革新之路:GraalVM 原生镜像

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Java 主导着企业级应用。但在云计算领域,采用 Java 的成本比它的一些竞争对手更高。原生编译降低了在云端采用 Java 的成本:用它创建的应用程序启动速度更快,使用的内存更少。

Java 主导着企业级应用。但在云计算领域,采用 Java 的成本比它的一些竞争对手更高。原生编译降低了在云端采用 Java 的成本:用它创建的应用程序启动速度更快,使用的内存更少。

那么,Java 用户的问题来了:原生 Java 是如何改变开发方式的?我们在什么情况下应该切换到原生 Java?什么情况下又不应该切换?我们应该使用什么框架?本系列文章将回答这些问题。

本文是“Native Compilations Boosts Java”系列文章的一部分。你可以通过订阅RSS接收更新通知。

GraalVM 自三年前发布以来,引发了一场 Java 开发革命。GraalVM 最常被讨论的特性之一是它的原生镜像是基于提前(AOT)编译技术。它提升了原生应用程序的运行时性能,同时保持开发人员熟悉的生产力方式和 Java 生态系统工具不变。

传统的 Java 应用程序执行方式

Java 平台最强大、最有趣的一个地方是 Java 虚拟机(JVM)执行代码的方式,它提供了出色的峰值性能。

在第一次运行应用程序时,JVM 会解释代码并收集剖析信息。尽管 JVM 解释器的性能很好,但还是不如运行已编译的代码快。这就是为什么 Oracle 的 JVM(HotSpot)也包含了即时(JIT)编译器,它可以在程序执行时将应用程序代码编译成机器码。因此,如果你的代码经过“预热”——被频繁执行,就会被 C1 编译器编译成机器码。然后,如果它们仍然执行得很频繁,并且达到某些阈值,就会被顶层的 JIT 编译器(C2 或 Graal 编译器)编译。顶层编译器会根据哪些代码分支执行得最频繁、循环执行的频率以及多态代码中使用了哪些类型来执行优化。

有时候,编译器也会进行推测性优化。例如,JVM 会根据收集到的剖析信息生成优化、编译过的方法。但是,由于 JVM 是动态执行代码的——如果它所做的假设变成无效的——JVM 将进行反优化:它将忽略已编译的代码并恢复到解释模式。正是这种灵活性让 JVM 变得如此强大:从快速执行代码开始,利用优化编译器来优化频繁执行的代码,并通过推测进行更积极的优化。

乍一看,这似乎是运行应用程序的一种理想的方法。然而,就像其他大多数事情一样,即使是这种方法也存在权衡,也需要付出成本。JVM 在执行某些操作(例如验证代码、加载类、动态编译和收集剖析信息)时,它需要进行复杂的计算,需要消耗大量的 CPU 时间。除了这个成本之外,JVM 还需要相当大的内存来存储剖析信息,在启动时也需要相当可观的时间和内存。随着许多公司将应用程序部署到云端,这些成本变得越来越重要,因为启动时间和内存直接影响部署应用程序的成本。那么,有没有一种方法既能减少启动时间和内存使用,又能保持我们都喜欢的 Java 生产力、库和工具呢?

答案是“是”,这就是 GraalVM 原生镜像所要做的事情。

大赢家 GraalVM

10 年前,GraalVM 是 Oracle Labs 的一个研究项目。Oracle Labs 是 Oracle 的一个研究和开发分支,主要研究编程语言和虚拟机、机器学习和安全、图形处理等领域。GraalVM 就是一个很好的例子——它以多年的研究和 100 多篇发表的学术论文为基础。

这个项目的核心是 Graal 编译器——一个全新的、高度优化的现代编译器。由于采用了多种高级优化手段,在许多情况下,它生成的代码比 C2 编译器更好。其中的一种优化是部分转义分析:如果分支中的对象没有转义编译单元,就通过标量替换移除不必要的堆对象分配,Graal 编译器会确保分支中有转义的对象一定存在于堆中。

这种方法减少了应用程序的内存占用,因为堆上的对象更少了。它还可以降低 CPU 负载,因为垃圾回收更少了。此外,GraalVM 的高级推测功能利用动态运行时反馈生成更快的机器码。通过推测程序的某些部分在程序运行期间不会被执行,让代码执行变得更加高效。

你可能会惊讶地发现,GraalVM 编译器的大部分代码是用 Java 写的。如果你看一下 GraalVM 的核心 GitHub 存储库,你会看到超过 90%的代码是用 Java 写的,这再次证明了 Java 是多么的强大和通用。

原生镜像的工作原理

Graal 编译器还是一种提前(AOT)编译器,可以生成原生可执行文件。既然 Java 是动态的,那么编译器究竟是如何做到的呢?

在 JIT 模式下,编译和执行同时发生,但在 AOT 模式下,编译器在构建期间(即执行之前)就完成了所有的编译。这里的主要思想是将所有“繁重的工作”——昂贵的计算部分——转移到了构建时,这样就可以一次性完成编译,然后生成的可执行文件在运行时就可以快速启动,并在一开始就做好准备,因为所有的东西都是预先计算和预先编译的。

GraalVM 的“native-image”工具接受 Java 字节码作为输入,并输出一个原生可执行文件。这个工具会通过假设对字节码执行静态分析。在分析过程中,工具会找出被应用程序使用的代码,并消除不必要的代码。

以下三个关键概念可以帮你更好地理解原生镜像的生成过程:

指向(Points-To)分析。GraalVM 原生镜像会确定哪些 Java 类、方法和字段在运行时是可访问的,并且只有这些内容会被包含在原生可执行文件中。指向分析从所有入口点(通常是应用程序的 main 方法)开始。分析过程会循环处理所有可触及的代码路径,直到到达一个固定点,然后分析结束。这不仅适用于应用程序代码,还适用于库和 JDK 类——将应用程序打包成自包含的二进制文件所需要的东西。

在构建时初始化。GraalVM 原生镜像默认在运行时进行类初始化,以确保正确的行为。但是,如果原生镜像可以证明某些类可以安全地初始化,它就会在构建时对它们进行初始化。这样一来,运行时初始化和检查就变得不必要,从而提高了性能。

堆快照。原生镜像中的堆快照是一个非常有趣的概念,值得专门写一篇文章。在镜像构建过程中,由静态初始化器分配的 Java 对象和所有可访问的对象都被写入镜像的堆。这意味着使用预先处理的堆可以更快地启动应用程序。有趣的是,指向分析会让镜像堆中的对象变得可触及,而构建镜像堆的快照会让更多方法可触及。因此,指向分析和堆快照将反复执行,直到到达一个固定的点:

image.png

原生镜像构建过程

在分析完成后,GraalVM 会将所有可触及的代码编译成特定于平台的原生可执行文件。可执行文件本身功能完备,不需要 JVM 来运行。因此,你得到的是 Java 应用程序的精简而快速的原生可执行版本:它具备完全相同的功能,但只包含必要的代码及其所需的依赖项。

但是,谁来负责处理内存管理和线程调度等问题呢?原生镜像中还包含了一个 Substrate VM——一个提供运行时组件(比如垃圾回收器和线程调度器)的精简 VM 实现。就像 GraalVM 编译器一样,Substrate VM 是用 Java 开发的,然后用 GraalVM 原生镜像的 AOT 编译技术将其编译成原生代码!

得益于 AOT 编译和堆快照,原生镜像为你的 Java 应用程序提供了一种全新的性能。接下来让我们来仔细看一看。

将 Java 启动性能提升到一个新的水平

你可能听说过原生镜像生成的可执行文件具有非常好的启动性能,那么究竟是怎样的性能呢?

即时启动。在 JVM 上运行时,代码需要经过验证、解释,然后(在预热之后)最终被编译,与此不同,原生可执行文件从一开始就带有优化的机器码。我喜欢用即时性能这个词来形容它——应用程序可以在启动的第一毫秒内执行有意义的任务,不需要任何分析或编译开销。

image.png

JIT 和原生镜像的启动过程对比

内存效率。原生可执行文件不需要 JVM 及其 JIT 编译器,也不需要用于代码、分析文件数据和字节码缓存的内存。它只需要用于可执行文件和应用程序数据的内存。这里有一个例子:

image.png

JIT 和原生镜像使用的 CPU 和内存对比

上图显示了 Web 服务器在 JVM 上(左)和作为原生可执行文件(右)的运行时行为。蓝绿色的线表示使用了多少内存:在 JIT 模式下是 200MB,而原生可执行文件是 40MB。红线表示 CPU 活动:JVM 在热身 JIT 活动期间使用了大量 CPU,而原生可执行程序几乎不使用 CPU,因为所有昂贵的编译操作都发生在构建时。这种快速且资源高效的运行时行为让原生镜像成为一种很棒的部署模型,在更短的时间内使用更少的资源,可以显著降低成本——适用于微服务、无服务器和云端工作负载。

文件体积。原生可执行文件只包含必需的代码。这就是为什么它比应用程序代码、库和 JVM 的总和要小得多。在某些场景中,例如在资源受限的环境中,应用程序的体积可能是一个很重要因素。UPX等工具可以进一步压缩原生可执行文件的体积。

峰值性能与 JVM 相当

那么峰值性能如何呢?既然一切都是提前编译的,那么原生镜像如何在运行时优化峰值吞吐量?

我们正在努力确保原生镜像提供良好的峰值性能和快速启动。已经有一些方法可以提高原生可执行文件的峰值性能:

基于分析的优化。由于原生镜像会提前优化和编译代码,所以默认情况下它无法在应用程序运行时访问运行时分析信息来优化代码。解决这个问题的一种方法是进行基于分析的优化(Profile-Guided Optimization,PGO)。开发人员可以运行应用程序,收集分析信息,然后将其反馈给原生镜像生成过程。“原生镜像”工具基于这些信息根据应用程序的运行时行为优化可执行文件的性能。PGO 包含在 GraalVM Enterprise(这是 GraalVM 的商业版本,由 Oracle 提供)中。

原生镜像的内存管理。原生镜像生成的可执行文件的默认垃圾回收器是 Serial GC,这对小内存堆的微服务来说是最优的。当然,还有其他 GC 选项:

Serial GC 提供了一个新的策略,可以为年轻代分配幸存者空间,从而减少应用程序运行时的内存占用。经过我们的测试,自从引入这个策略以来,典型的微服务工作负载(如 Spring Petclinic)的峰值吞吐量改进高达 23.22%。

或者,你也可以使用低延迟的 G1 垃圾回收器,从而获得更高的吞吐量(包含在 GraalVM Enterprise 中)。G1 最合更大的堆。有了 PGO 和 G1 GC,原生可执行文件的峰值性能可与 JVM 媲美:

image.png

Renaissance 和 DaCapo 测试基准

有了这些选项,就可以利用原生镜像最大化应用程序的各个性能维度:启动时间、内存效率和峰值吞吐量。

反射、配置和其他

由于原生镜像是执行 Java 应用程序的一种全新的方式,所以有几个地方需要注意。

有人说 GraalVM 原生镜像不支持反射,这不是真的。

原生镜像会基于一些假设进行静态分析。因此,要启用 Java 的动态特性(如反射),需要进行额外的配置。在对 Java 应用程序进行静态分析时,它会尝试检测和处理反射 API 调用。然而,通常情况下,这种自动分析是不够的,而且在运行时通过反射访问的程序元素必须通过配置来指定。你可以手动创建这些配置,也可以利用原生镜像跟踪代理。当程序运行在 JVM 上,代理会跟踪动态特性,并生成配置文件。原生镜像工具使用这个文件来包含调用了反射 API 的部分。虽然代理可用于获得初始的配置,但我们还是建议在必要时通过手动检查来完成这个过程。

在使用 Java 本地接口(JNI)、动态代理对象和类路径资源时,可能需要类似的配置。你也可以使用这个跟踪代理来获得这些配置。

最后,你可以使用GraalVM Dashboard,一个可视化原生镜像编译的 Web 应用程序,可以用它来发现原生可执行文件中包含了哪些包、类和方法,还可以识别哪些对象在堆中占用了最大的空间。

改变 Java 云端部署

原生镜像将改变云端部署,它对应用程序的资源消耗产生了很大的影响。我们知道,原生镜像生成的原生可执行文件启动快,需要的内存少。对于云端部署来说,这到底意味着什么? GraalVM 如何帮助最小化 Java 容器镜像?

运行原生镜像生成的应用程序不需要 JVM:它们可以是自包含的,包括应用程序执行所需的所有东西。这意味着你可以将应用程序放入一个苗条的 Docker 镜像中,并且它本身将具备完整的功能。镜像大小取决于应用程序要完成的任务以及它包含哪些依赖项。一个使用 Java 微服务框架构建的“Hello, World!”应用程序大约有 20MB。

你还可以用原生镜像构建全静态或部分静态的可执行文件。部分静态的原生可执行文件被静态链接到所有的库,除了容器镜像提供的“libc’。你可以用 distroless 容器镜像进行轻量级部署。disroless 镜像只包含运行应用程序所需的库,不包含 shell、包管理器和其他程序。举个例子,你的 Dockerfile 可能是这样的:

image.png

对于一个完全自主的部署(甚至不需要容器镜像提供的 libc)来说,你可以静态地将应用程序链接到“musl-libc”。你可以把它放在“FROM scratch”的 Docker 镜像中,因为它是完全自包含的。

在生产环境中使用原生镜像

到目前为止,我们已经讨论了如何最大化原生镜像生成的应用程序的性能,并考虑了在构建过程中可以应用的一些有用的技巧。除此之外,我们还可以做些什么来最大限度地利用应用程序呢?是的,有很多。

为了简化原生可执行文件的构建、测试和运行,可以使用 GraalVM 团队提供的 Maven 和 Gradle插件。此外,这些插件支持原生 JUnit 5 测试,并且是与 JUnit、Micronaut 和 Spring 团队合作开发的,充分彰显了 JVM 生态系统的协作关系。

要在你的 GitHub Action 工作流中设置 GraalVM 原生镜像,可以使用GraalVM的GitHub Action。可配置的 Action 支持多个 GraalVM 版本和开发者构建,并可设置好完整的 GraalVM 和特定组件。

现在我们来说一下工具。在开发 Java 应用程序时,你可以使用常规的工具。你可以使用任意的 IDE 和 JDK(包括 GraalVM JDK)来构建、测试和调试应用程序,然后使用 GraalVM 原生镜像工具来进行最终的原生编译。根据应用程序复杂程度的不同,原生镜像编译可能需要一些时间,因此建议将其作为最后一个步骤。不过,我们正在为原生镜像开发一种快速开发模式,它将跳过一些优化步骤,以此来缩短编译时间。

尽管你可以基于 JVM 开发应用程序,然后在稍后的开发过程中构建原生可执行文件,但我们收到了很多来自社区的请求,要求改进构建时间和资源使用。在过去的几个版本中,我们针对这个问题做了很多工作。在最新发布的 GraalVM(22.0)中,你可以在大约13.8秒内将一个 hello-world Java 应用程序生成一个原生可执行文件,可执行文件的大小大约为 5MB。我们还减少了大约 10%的内存使用。

要调试原生镜像生成的可执行文件,可以在命令行中使用“gdb”(在 Linux 和 macOS 上),或者使用 GraalVM 的 VS Code 扩展。这个教程提供了使用说明。

要监控原生可执行文件的性能,请使用 JDK Flight Recorder。对原生镜像的全面支持仍在开发当中,不过你已经可以用它来观察自定义事件和系统事件。

如果要进行额外的性能监控,可以生成原生可执行文件的堆转储,然后使用 VisualVM 等工具对其进行分析。这是 GraalVM Enterprise 的一个特性。

哪些 Java 框架采用了原生镜像

如果没有 Java 框架的支持,开发行业级应用程序将是非常困难的。幸运的是,现在有很多可用的框架。所有主流的框架都支持原生镜像(按字母顺序列出):Gluon Substrate、Helidon、Micronaut、Quarkus 和 Spring Boot。所有这些框架都利用 GraalVM 原生镜像显著改善了应用程序的启动时间和资源使用,成为高效的云端部署工具。本系列的后续文章将介绍框架是如何使用 GraalVM 原生镜像的。

原生镜像的未来

自从第一次公开发布以来,原生镜像已经取得了巨大的进步。它被 Java 框架广泛采用,云供应商也将原生镜像作为 Java 运行时,许多库也都使用了原生镜像。我们对开发者的体验做了一些改变,我们去年的研究表明,70%使用 GraalVM 的开发者已经在用它来构建和发行原生可执行文件。

对于原生镜像的新特性和改进,我们有很多想法,包括:

支持更多的平台;

简化 Java 库的配置和兼容性;

继续优化峰值性能;

继续与 Java 框架团队合作,充分利用所有的原生镜像特性,开发新的特性,提高性能,并确保良好的开发体验;

引入更快的开发编译模式;

支持 Loom 的虚拟线程;

让 IDE 支持原生镜像配置和基于代理的配置;

进一步提高 GC 性能并添加新的 GC 实现。我们要感谢社区和我们的合作伙伴帮助我们推动原生镜像的发展,让它对每个 Java 开发者起到越来越大的作用。如果你想在原生镜像中看到新的功能或改进,请通过 GraalVM 的社区平台与我们分享你的反馈!

作者简介:

Alina Yurenko 是 Oracle Labs(Oracle 的一个研究和开发部门)的 GraalVM 开发者布道师。她有开发者关系的工作经验,现在加入了 GraalVM 团队,与它的全球社区一起工作。

相关实践学习
通过workbench远程登录ECS,快速搭建Docker环境
本教程指导用户体验通过workbench远程登录ECS,完成搭建Docker环境的快速搭建,并使用Docker部署一个Nginx服务。
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
1月前
|
Java
java原生发送http请求
java原生发送http请求
|
1月前
|
数据采集 小程序 数据可视化
智慧校园电子班牌管理系统源码 Java Android原生
家长通过家长小程序端随时了解孩子在校的情况,实时接收学生的出勤情况,学生到校、离校时间。随时了解学生在校的表现、学生成绩排名,及时与教师沟通,关注孩子的健康成长。
43 0
智慧校园电子班牌管理系统源码 Java Android原生
|
1月前
|
JSON 自然语言处理 Java
Java原生操作Elasticsearch
Java原生操作Elasticsearch
50 0
|
1月前
|
存储 网络协议 Java
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)(二)
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)
43 0
【Java】BIO源码分析和改造(GraalVM JDK 11.0.19)(二)
|
2天前
|
网络协议 JavaScript 前端开发
Java一分钟之-GraalVM Native Image:构建原生可执行文件
【6月更文挑战第13天】GraalVM Native Image是Java开发的创新技术,它将应用编译成独立的原生可执行文件,实现快速启动和低内存消耗,对微服务、桌面应用和嵌入式系统有重大影响。本文讨论了如何使用Native Image,包括常见挑战如反射与动态类加载、静态初始化问题和依赖冲突,并提供了解决方案和代码示例。通过合理规划和利用GraalVM工具,开发者可以克服这些问题,充分利用Native Image提升应用性能。
28 5
|
3天前
|
传感器 小程序 搜索推荐
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
通过电子班牌设备和智慧校园数据平台的统一管理,在电子班牌上,班牌展示、学生上课刷卡考勤、考勤状况汇总展示,课表展示,考场管理,请假管理,成绩查询,考试优秀标兵展示、校园通知展示,班级文化各片展示等多种化展示。
25 0
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
|
3天前
|
XML Java 数据格式
【JAVA日志框架】JUL,JDK原生日志框架详解。
【JAVA日志框架】JUL,JDK原生日志框架详解。
5 0
|
3天前
|
安全 Oracle Java
Java一分钟之-GraalVM:高性能运行时与编译器
【6月更文挑战第12天】GraalVM是Oracle实验室的高性能运行时和编译器,支持Java、JavaScript等多语言,提供即时编译和提前编译技术,提升应用性能和跨语言互操作性。其核心亮点包括多语言支持、高性能、Native Image(AOT编译)和安全沙箱。常见问题涉及Native Image构建失败、反射与动态加载处理及资源消耗误解。解决这些问题需要详细阅读官方文档、利用GraalVM工具链和参考社区资源。通过Native Image,开发者可以构建接近零启动时间的原生应用。GraalVM是打破语言壁垒、提升应用效率的有力工具,随着生态发展,将在技术领域发挥更大作用。
19 1
|
3天前
|
Kubernetes Cloud Native Java
Java一分钟之-Quarkus:Kubernetes原生的Java框架
【6月更文挑战第12天】Quarkus是面向Kubernetes的Java框架,以其超快启动速度和低内存占用著称。核心特性包括AOT编译实现毫秒级启动、优化的运行时模型、与Kubernetes的无缝集成及丰富的扩展库。常见问题涉及Maven依赖管理、热重载机制理解和配置文件的忽视。解决这些问题的关键在于深入学习官方文档、使用Dev UI调试和参与社区交流。通过代码示例展示了如何快速创建REST服务。掌握Quarkus能提升开发效率,适应微服务架构。
18 0
|
9天前
|
Cloud Native Java Docker
java一分钟之-Docker化Java应用:Dockerfile与镜像构建
【6月更文挑战第6天】本文探讨了Docker在Java应用部署中的重要性,强调了Dockerfile在保证环境一致性和提升部署效率上的作用。Dockerfile是自动化构建Docker镜像的文本文件,它的使用能实现标准化、可重复性和透明度。文章指出了编写Dockerfile时的常见问题,如指令误用、镜像体积过大和安全性不足,并提供了相应的解决策略。通过一个Spring Boot应用的实战示例,展示了如何编写Dockerfile和构建镜像。总之,掌握Dockerfile和镜像构建技巧对于优化Java应用的云原生部署至关重要。
36 0