AOT 编译让 Java 更强大

我尝试了上一篇文章,探索了为一个简单的 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 性能来说非常有前途。然而,对于大多数开发人员来说,这似乎也相当具有挑战性。

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值