Java如何协调代码的快速启动和实时优化两者之间平衡?
Java虚拟机(JVM)的一个伟大之处在于,它能够使Java应用程序的性能适应其使用方式。
它可以找出你的代码中哪些部分是经常使用的,然后通过其及时编译代码的能力(JIT)来优化代码。
但这也意味着,它必须先弄清楚这些部分,然后才能将这些部分编译成更快的代码。
而这需要时间,也就是说,你不能简单地运行你的代码,并认为JVM会立即优化它,使其运行得尽可能快。
这是因为在你的应用程序能够以最佳状态运行之前,它需要时间来预热JVM。
现代应用两难
如果你有一个长期运行的应用程序,预热时间,可能在几秒钟到几分钟的范围内,通常没有问题。
但是,这些天来,Java应用程序经常被用于微服务环境,这意味着你可能有很多小的应用程序,它们只是运行了很短的时间,但会经常被重新启动。
在这种情况下,JVM的预热时间不是很有帮助,因为在微服务将被再次关闭之前,JVM可能甚至还没有预热好。
解决预热问题的一个方法是提前编译你的应用程序,并从中创建一个可以快速启动的本地镜像。
但原生镜像的缺点是,一旦你的代码被静态编译为原生代码,你就会失去JVM可以进行的运行时优化的能力。
检查点的协调恢复
那么问题来了,是否有办法保留JVM但减少其启动时间?
答案是,是的,有:使用CRaC,即检查点的协调恢复。
Azul公司的高级软件工程师Anton Kozlov是围绕这个主题的OpenJDK提案的幕后推手,你可以在相关的OpenJDK页面上找到更多关于这个项目的信息。
CRaC项目的重点是开发一个Java API,使得保存和恢复JVM的状态成为可能,包括当前运行的应用程序。
这个CRaC API是为了协调,加强检查点/恢复,尽管在技术上检查点/恢复在某些情况下不需要协调也可以实现。
使用这种方法可以使启动时间从几百秒大幅减少到几十毫秒。
该建议依赖于Linux CRIU(Coordinated Resume In Userspace)项目,加上其他额外的方法。
检查点的创建
这个想法是用你的应用程序启动JVM,并对其进行预热,直到它达到最佳性能。
一旦达到这个状态,你就创建一个JVM的快照,即所谓的检查点。
检查点的创建意味着JVM的当前状态将被保存到文件系统的一组文件中。
现在你可以将JVM从这组文件中恢复到一个正在运行的实例,但不需要对它进行预热。
如果你考虑到部署在容器化环境中的微服务,你可以考虑启动一个容器,在容器内预热JVM,并通过停止容器创建一个检查点。
下次你启动这个容器时,它就可以从存储的检查点恢复JVM。
CRaC API
创建检查点需要你的应用程序释放其资源,如数据库连接、HTTP连接和打开的文件,否则检查点图像可能因依赖可能消失的资源而过时。
拟议的CRaC API提供了一些方法来帮助你在创建检查点之前释放资源,并在恢复检查点的JVM之后连接你的资源。
首先,你需要实现 "jdk.crac "包中的资源接口。
这个接口提供了两个方法,"beforeCheckpoint() "和 “afterRestore()”。
为了使其工作,你还需要通过调用 "Core.getGlobalContext().register() "将你的资源注册到一个全局上下文:
public class Main implements Resource { |
public Main() {
Core.getGlobalContext().register(Main.this);
}
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
// Free your resources here
}
@Override
public void afterRestore(Context<? extends Restore> context) throws Exception {
// Load your resources here
}
}
为了能够检查一切是否按预期工作,目前的实现会在你有开放的资源时抛出异常,例如开放的套接字。
当你触发检查点时,JVM的堆将被清理和压缩,这样JVM就处于安全状态。
CRaC项目还处理由JVM产生的文件。
因为它依赖于CRIU,CRaC项目与CRIU捆绑在一起,这意味着你不需要手动安装它。
检查点可以通过shell中的jcmd工具来创建,也可以从代码本身调用Core.checkpointRestore()。
这将创建检查点并退出应用程序。
注册资源是通过在检查点创建前和检查点恢复后通知一个全局上下文来完成的。
入门
- 获取已经包含 CRaC 功能的OpenJDK 构建。
- 在 GitHub 上获取我创建的基本示例,让您了解 CRaC 的工作原理。
简而言之,该示例将每 5 秒调用一次循环。在该循环中,它将检查 100000 次,如果 1 - 100000 之间的随机数是素数。
在进行实际计算之前,它会检查结果是否已经在缓存中。
如果找到缓存中的数字,则直接返回结果,如果没有,则计算结果,将其放入缓存中,然后返回。
在查看应用程序性能时,这将导致与普通应用程序类似的行为。
一开始,缓存是空的,这导致每个数字至少计算一次。随着时间的推移,性能会提高,因为缓存会越来越满。
有关如何设置 CRaC 和运行示例的信息可以在自述文件中找到。
关于 CRaC 的更多信息可以在这里找到:https://openjdk.java.net/projects/crac/https://wiki.openjdk.java.net/display/crac