我尝试了上一篇文章,探索了为一个简单的 hello world spring boot rest 应用程序加载的大约 6000 个类。尽管 Quarkus 版本似乎通过将数量减少到 3300+ 进行了优化,但它仍然太多了。在本文中,我将介绍如何使用 Java AOT 编译来消除 Java 中的死代码,从而显着提高性能。
该实验使用 Quarkus 作为框架。
如果您更喜欢观看视频以获取主要思想,您可以直接在本文中查看 youtube。
查看 Java 性能的两种视角
第一反应时间
Java 应用程序具有预热效果。当它们在 Kubernetes 这样的容器平台上运行时,这不是一个迷人的功能。
因为在高度动态的环境中,应用程序来得快去得也快,就像新陈代谢一样。在无服务器架构下,应用生命周期极快。因此,预热效应会使 Java 应用程序变得不和谐。
有两个众所周知的原因:
延迟初始化
传统上,延迟初始化是一种通过将任务推迟到请求到来来优化和节省内存和 CPU 资源的策略。Java 框架和服务器在历史上尤其以使用它而闻名,有时这种技术被积极使用。容器环境不支持此功能。
JVM 预热
这就是JVM的本质。JIT编译行为是有选择地编译热代码,即所谓的热点OpenJDK。这意味着它只会对热路径中的代码进行 2 级优化,而 JVM 在应用程序预热之前不会进行优化。
RSS(全进程内存消耗)
RSS(Resident Set Size)是指整个进程的内存消耗。
Java 应用程序通常占用大量内存,即使是 hello world 应用程序也是如此。假设一个弹簧启动的hello world程序;您将需要:
JVM(只有 JVM 11 大小为 260+M)
应用程序服务器,例如 Tomcat 或 vertx、netty 或 undertow,具体取决于您的选择,使您能够运行服务器和 servlet 事物。
一些智能框架使您能够进行智能 Java 编程。
想象一下,加载的内容与启动hello world逻辑无关。
但为什么现在重要,而不是以前呢?这是因为在容器和微服务普及之前,成本是分摊的。多个 Java 应用程序(如“war”或单个大型单体应用程序共享单个 JVM 和 Application Server)。
Java AOT 编译消除死代码
这个想法并不新鲜。Oracle 于 2018 年 4 月宣布了 Graalvm 项目;它包括一个本机映像工具,可以将基于 JVM 的应用程序转换为本机执行二进制文件。
它看起来像这样:
$ native-image -jar my-app.jar \
-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
-J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 \
-H:FallbackThreshold=0 \
-H:ReflectionConfigurationFiles=...
-H:+ReportExceptionStackTraces \
-H:+PrintAnalysisCallTree \
-H:-AddAllCharsets \
-H:EnableURLProtocols=http \
-H:-JNI \
-H:-UseServiceLoaderFeature \
-H:+StackTrace \
--no-server \
--initialize-at-build-time=... \
-J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-J-Dio.netty.leakDetection.level=DISABLED \
-J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
-J-Dsun.nio.ch.maxUpdateArraySize=100 \
-J-Dio.netty.allocator.maxOrder=1 \
-J-Dvertx.disableDnsResolver=true
当我通过以下方式构建应用程序时:
./mvnw 包 -Pnative
您将获得:
Quarkus-maven-plugin 做了所有艰苦的工作,并自动为我们配置了原生构建选项的所有配置。
性能对比明显;下图引用自 quarkus.io:
现在让我们关注 Quarkus 原生模式(绿色)和 Quarkus JVM 模式(蓝色)之间的对比。(如果你对Quarkus在JVM模式下的不同表现感兴趣,我在上一篇文章中探讨了如何构建时引导提高性能。
我将测试一个真实世界的用例:一个 todo 应用程序使用 hibernate 连接 PostgreSQL。
基本上,在视频中,我演示的是:
与纯模式相比,RSS 比 JVM 低 7 倍。
我可以轻松地在本机模式下启动 250 个应用程序实例。但是,有 50 个 JVM 模式实例导致我的 CPU 非常繁忙。
第一反应的时间差异也很大。尽管本机与 JVM 的可伸缩性是 250 到 25。
因此,Java 原生模式的运行性能非常有前途。
如果您更喜欢编写代码并亲自查看,这里是代码和指南。
Java AOT 的工作原理
从图表和实验来看,Java AOT 编译性能很好。现在让我们看看它是如何工作的。
首先,我们需要从传统上理解;Java 使用 JIT 机制来运行 java 字节码。
当应用程序开始运行时,java 命令将启动 JVM,并在运行时解释和编译行为。为了动态优化此编译,我们进行了大量优化。使用大量的代码缓存技术。实现这一点的一大驱动因素是实现跨平台兼容性。
AOT 在运行时(即在构建时)将字节码直接编译为本机二进制文件的方式不同。因此,您将绑定到特定的硬件体系结构。它只编译为 x86 架构,并放弃了另一个 arch 兼容性。这种权衡是值得的,因为我们只针对已经在云中无处不在的 Linux 容器来构建我们的方案。
此解决方案面临两大技术挑战。
将“开放世界”Java 变成“封闭世界”Java
难度与您使用的 Java 依赖项数量成正比。经过 25 年的发展,Java 生态系统庞大而完整。
在这么多年里,java 之所以成为 java,是因为它是一种动态编译语言,而不是静态语言。它提供了许多功能,如反射、运行时的类加载、动态代理、运行时生成代码等。许多事实上的功能,如注解驱动的编程(声明式),CDI依赖于,java lambda依赖于那些Java动态性质。从头开始将字节码转换为本机二进制文件绝非易事。
在我的演示中,我使用 hibernate + PostgreSQL 来制作一个在本机模式下运行良好的 TODO 应用程序。这是因为 Quarkus 社区已经优化了大量库和框架。
如果你的应用程序可以接受现有的quarkus扩展列表(即优化的库,我通过运行quarkus 1.8.3.Final来生成列表),那么你的应用程序的Java AOT是高度可行的,一点也不困难。
Graalvm 命令映像的优化配置
幸运的是,quarkus-maven-plugin 已经顺利处理了这个问题。您需要做的是通过运行 mvn 来 -Pnative。我绝不愿意亲自动手去做原生图像命令。
总之,Java 超前编译对于提高 Java 性能来说非常有前途。然而,对于大多数开发人员来说,这似乎也相当具有挑战性。