Java虚拟机(JVM)提供了托管运行时环境,用于安全部署应用程序,其性能通常可以超过本机编译语言(如C和C ++)的性能。 通过即时(JIT)编译进行垃圾收集和自适应编译的内存管理是两个最突出的功能。
尽管使用字节码和JIT编译可以提供更好的峰值性能,但是对于某些类型的应用程序,达到该级别所需的预热时间可能会成问题。
在本文中,我们将研究Azul作为Zing JVM的一部分而开发的一组技术,以解决这些限制。
首先,让我们看一下在JVM上运行时的典型应用程序性能图。
![JVM应用程序](https://i-blog.csdnimg.cn/blog_migrate/ed8c3beb28b7710cd32b50f74f6875b3.png)
该图并不理想,因为应用程序开始时性能降低,并且JVM需要时间才能发挥其全部潜能。 该图可以分为三个不同的部分: 让我们看一下JVM内部每种情况的变化。
- 当应用程序启动时,JVM必须加载并初始化必要的类。 完成此操作后,JVM将在main()入口点开始执行。 由于JVM是虚拟机 ,因此它不会使用与运行它的物理机相同的指令集。 因此,有必要将类文件的字节码转换为物理CPU的指令集。 这称为字节码解释 。 必须对每个执行的字节码重复此操作,这会导致性能比本地编译的应用程序低得多。 这在很大程度上归因于Java首次发布时速度慢的声誉。
上图以黄色显示了已解释的模式。
- 为了减轻在解释模式下运行的问题,JVM在内部记录了每种方法调用频率的统计信息。 这样,它就可以为重复调用的方法(例如,长时间运行的循环)中的代码识别热点 (因此称为Oracle JVM)。 当方法调用计数达到定义的阈值时,JVM将方法传递给内部编译器,该编译器称为即时编译器(通常称为JIT)。
JVM在此阶段使用的编译器称为C1(以前,它也称为客户端编译器)。 C1 JIT旨在尽快生成代码,以便快速提高这些方法的性能。 为此,C1将仅应用最简单的优化,这些优化不需要其他配置数据或需要很长时间才能生成。 如上图的绿色部分所示,随着编译更多方法,性能逐渐提高。 在运行此代码时,JVM将收集有关如何使用该方法以及如何执行代码的全面分析数据。
- 在调用方法的次数达到第二个阈值时,JVM将使用其他JIT编译器重新编译该方法。 在Zing的情况下,这是基于开源LLVM项目的Falcon JIT。 默认的OpenJDK二级JIT编译器为C2,它非常旧且难以增强。
Falcon是比C1更复杂的编译器。 它使用在执行C1生成的代码期间收集的概要分析数据以及来自JVM的其他内部数据,将最大程度的优化应用于其生成的代码。 这是图形的蓝色部分,一旦所有常用的方法都已编译,性能最终将达到最高水平。 此时,该应用程序被视为已预热 。
现在,我们了解了JIT编译在JVM中的工作方式,可以采取什么措施来减少其对应用程序启动性能的影响? Azul开发了两种技术,使Zing JVM能够减轻预热效果。
关于如何解决此问题的一个常见建议是让应用程序运行,直到所有常用的方法均已JIT编译,然后让JVM将已编译的代码写入文件中。 重新启动应用程序时,可以重新加载以前编译的代码,并且应用程序将以停止之前的速度运行。
听起来不错的解决方案,但有两个重大缺点:
- 尽管代码是为正在运行的应用程序编译的,但不能保证在重新启动JVM时它仍然有效。 为何使用断言是一个很好的例子。 如果在禁用断言的情况下运行应用程序,则JIT将消除代码的相关部分。 如果随后在启用断言的情况下重新启动应用程序并使用先前编译的代码,则断言将丢失。
- JVM规范有一个关于JVM必须如何工作的精确定义。 它包含在Java SE规范中,该规范是根据JCP在相关JSR中创建的。 这定义了JVM运行应用程序时必须执行的特定任务。 必须先显式加载和初始化类,然后才能使用它们。 同样,如果使用了先前编译的代码,这可能会使JVM的正确操作无效。
Azul的ReadyNow! 这项技术采用了一种不同的方法,可以确保正在执行的代码和JVM的启动顺序都完全正确。
要实现此ReadyNow! 记录正在运行的应用程序的配置文件。 可以随时获取配置文件,以便用户可以决定他们的应用程序何时以所需的级别运行。 可以拍摄多个配置文件快照,以便用户可以在重新启动应用程序时选择所需的配置文件。
该配置文件记录五段数据:
- 已加载的所有类的列表。
- 已初始化的所有类的列表。
- 在执行C1 JIT编译代码期间收集的概要分析数据。
- C1和Falcon JIT均执行编译。
- 失败并导致代码未优化的推测优化列表。
再次启动应用程序时,此数据将用作JVM的高级知识,以执行以下步骤:
- 加载配置文件中列出的所有引导程序和系统类。
- 初始化那些已加载类的安全子集。 被认为是安全的类是JMV规范允许的类。
- 确定所需的类加载器后,将立即加载配置文件中的其他类。 如前所述,由于Java平台的动态性质,这是必需的。
- 分析和推测性优化数据用于使用Falcon JIT编译所需的方法。
所有这些都在应用程序在main()入口点开始执行之前发生。
这样的结果是,当应用程序开始执行时,几乎所有热门方法都已使用Falcon JIT进行了编译。 通过使用概要分析数据,可以对代码进行大量优化,并使用已知有效的推测性优化(也可以避免不必要的优化)。 性能开始于非常接近收集概要文件时的水平。 由于此过程的工作方式受到一些限制,因此应用程序通常仅需要执行少量事务即可使其全速运行。
但是,此方法确实会产生影响。 在应用程序何时可以开始处理事务之前,JVM还有很多工作要做。
为了减轻这种影响,Azul开发了Compile Stashing
正如我们已经看到的,在重新启动应用程序时,不可能简单地保存已编译的代码然后重新加载它。 但是,可以保存已编译的代码,并有效地将其用作缓存。
方法的字节码与保存的性能分析数据组合在一起,因此可以将它们转换为编译器使用的中间表示(IR)。 在编译代码时,JIT将调用JVM,以帮助其做出有关可以使用的优化的决策。 例如,要确定是否可以内联方法,JIT必须首先确定该方法是否可以进行虚拟化,这需要查询JVM。 JIT完成对方法的分析后,便可以最大程度的优化对其进行编译。
此过程是完全确定的。 给定相同的方法字节码和配置文件数据作为输入以及对JVM的相同查询集,JIT编译器的输出将始终相同。
编译存储补充ReadyNow! 除了记录配置文件外,还将当前编译方法的本机代码以及VM回调的查询和响应写入文件。 当应用程序再次启动时,ReadyNow! 像以前一样,基于配置文件加载并初始化可以的类。 但是,保存的已编译方法现在用作缓存,以减少对显式编译的需求。 操作流程图如下所示:
![JVM应用程序](https://i-blog.csdnimg.cn/blog_migrate/c2e6f30929a3540abbb83110a2594eb3.png)
好了! 将IR用于方法的字节码,并查询编译期间使用的VM的组合,以确定存储的编译代码是否匹配。 如果是这样,则可以从“编译存储区”返回该代码。 如果由于某种原因输入与编译请求不匹配,则可以像以前一样将其传递给Falcon JIT。 重要的是要注意,使用此技术不会使JVM规范中有关应用程序初始化的任何要求无效。
测试表明,使用“编译存储”时,ReadyNow!需要的编译时间! 最多可减少80%,并最多减少60%的CPU负载。
如您所见,ReadyNow! 和Compile Stashing通过记录类加载和概要分析数据,无效的推测性优化和编译的代码来解决应用程序预热时间的问题。 重新启动应用程序时使用所有这些组件可以大大减少应用程序达到最佳性能水平所需的时间和CPU负载。
Zing是启动速度快,保持速度快且运行速度更快的JVM。
准备开始使用更好的JVM了吗?
翻译自: https://www.javacodegeeks.com/2019/06/faster-jvm-application-warm-zing.html