随着 Spring Boot 3.0 的发布,我们获得了对 GraalVM 原生构建的官方支持。这是否意味着我们终于可以摆脱 JVM 的开销?本机构建如何提高应用程序的性能?权衡在哪里,值得吗?在这篇文章中,我们将尝试获得这些问题的一些答案。
Spring Native项目现在正式成为 Spring Boot 3.0 版本的一部分。有了它,在我们这边,我们可以直接将 Spring Boot 项目编译成操作系统原生的可执行文件,完全省略 Java 的 VM。因此现在我们可以在没有安装 JRE 的系统上轻松运行二进制文件。
通过GraalVM AOT(提前)编译,我们可以做到这一点。由于它随 Spring Boot 3.0 免费提供,现在是开始直接在裸机上运行您的应用程序的最佳时机……或在云的虚拟化计算服务上运行。
让我们首先查看空白应用程序,直接从Spring Initializer开始,没有其他依赖项,以获取参考点。我将测试编译时间、最终文件大小将是本机或 JAR、启动时间和进程分配的内存。
编译时间
将应用程序编译为本机映像比构建 JAR 花费的时间要长得多。在我的测试中,编译原生镜像平均需要1 分钟 37 秒,而构建 JAR 只需要4 秒——这几乎是原来的 25 倍。这很多,但我们应该预料到,AOT 会在编译步骤执行一些优化。在默认的 HotSpot 中,JVM 的 JIT 方法将那些事情推迟到运行时。AOT 编译器也在做 JIT 永远不会尝试做的事情,例如,AOT 将检查哪些资源从未使用过,并且可以从最终构建中丢弃类。使用 JIT,我们总是将类路径中的所有内容放入最终的 JAR 中。
文件大小
使用本机构建时,应用程序的文件大小也会明显变大。在我的测试中,空白应用占用45.3MB的磁盘空间,而 JAR 仅重14.4MB。大小差异是由于本机构建是独立的可执行文件这一事实造成的。它不需要启动任何其他依赖项,如 JRE。这意味着我们必须将 JRE 可能需要的所有内容打包到二进制文件中。JAR 将利用 JRE,因此它只能包含我们应用程序的源代码。
启动时间
Spring Boot 应用程序因启动时间长而臭名昭著。类路径扫描是导致问题的原因之一。由于 AOT 将该过程推到编译时,我们在这方面看到了巨大的改进。在我的测试中,JAR 需要0.774 秒才能启动,而本机二进制文件只需要0.017 秒。提高了45 倍。
内存使用情况
内存使用似乎也有所改善。空白 JAR 必须分配125MB内存,而本机构建只需要27MB。那是我们可以看到退出 JVM 的好处的另一个地方。
示例应用
让我们测试一些更接近现实生活场景的东西。
这是一个示例应用程序,它只是重新打包 DTO 并将它们从左推到右。
没有JIT预热 warm up:
JIT:606ms
AOT:681ms
打开JIT的预热:
JIT:373ms
AOT:642ms
从这些数字可以看出,如果我们让JIT预热,同样的任务只需执行原来的60%的时间。我们在本地构建上没有同样的改进,无论我们花多少时间预热代码,执行时间都是相似的。
如果你正在考虑在prod上利用原生构建,请仔细对你的代码进行基准测试。
如果您严重依赖JIT优化(AOT无法实现),您可能会惊讶于您的应用程序运行速度变慢。
结论
总结一下 AOT 构建,我们用更长的编译时间和更大的文件大小来换取更快的启动时间和更低的内存使用量。我能想到一个地方,这种权衡非常有意义——它是云!如果您在云端运行代码,请尝试一下。请记住,要在 Spring Boot 中原生使用 GraalVM,您需要升级到 3.0 或更高版本。这本身就是一个挑战,特别是因为 Spring Boot 3.0 使用 Java 17 作为基线。