OpenJ9 和 IBM J9 是来自默认 Oracle HotSpot JVM 的不同 JVM 实现。使用现代的 adoptopenjdk 预置 Docker 镜像,你可以轻易地切换和测试不同的组合,并且可以为你选择合适的 JVM。
这个传言看起来是真的,OpenJ9 在内存使用方面看起来已经领先 HotSpot 了。HotSpot 似乎在 CPU 方面具有优势。
在 Java 的世界中,大多人都熟悉 OpenJDK。这是一个完全的 JDK 实现,包括对 HotSpot JVM 引擎的实现。不是很多开发者了解或尝试选择 HotSpot。询问周围的同事后,他们都记得 JRockit 这个名字,但没有人提起 IBM J9 及(或) Eclipse OpenJ9。
我已经了解到了 OpenJ9 擅长于内存管理 ,而且在云/容器中的使用上已经经过了精简。OpenJ9 是一个独立的对 JVM 的实现。它源于 IBM 的 Java SDK/IBM J9,它的历史能追溯到 OTI Technologies Envy Smalltalk(感谢 Dan Heidinga!)。
随着微服务使用率的提升(而且 Java 中的大多数服务都 不是 特别小)。我认为它将会再次变成一个热门话题!
在 Docker 时代之前,比较不同的 JVM 版本是相对困难的。你需要下载、安装、编写脚本和运行所有相关项。但现在很多提前制作好的镜像可以通过在线获得。
这里是我关于如何测试 JVM 的想法:
创建一个简单的 Spring Boot 应用程序
在各种 Docker 镜像中启动该应用程序
在启动和 GC 后测量内存使用情况
测量运行小型 CPU 密集型测试所需的时间
这绝不是一个彻底的测试或基准测试,但它应该能给我们一个我们可以从虚拟机中获得什么的大致想法。
我创建的 Spring Boot 应用包含了下面的端点:
一个 REST 端点调用 GC( 尽量让它合理)
一个 REST 端点创建了 1000 个大 型随机数组并对其排序,返回运行时长(单位为 ms)
下面列出了 CPU 测试:
@RestController public class LoadTestController { @RequestMapping("/loadtest") public LoadTestResult loadtest() { long before = System.currentTimeMillis(); Random random = new Random(); for(int i = 0; i < 1000; i++) { long[] data = new long[1000000]; for(int l = 0; l < data.length; l++) { data[l] = random.nextLong(); } Arrays.sort(data); } return new LoadTestResult(System.currentTimeMillis() - before); } }
就这个测试是否合理和切题…我们可以无休止地争论,但是对于可以期待它是什么样的表现形式,它能给我们一些基本概念。如果传言中的内存提升是真的,可能会出现性能干扰吗?是否存在性能折中的情况?
我决定对以下镜像进行测试。
首先我们有 8/9/10/11 版本的(轻量级) openjdk 镜像:
openjdk:8-slim
openjdk:9-slim
openjdk:10-slim
openjdk:11-slim
接下来是 8/9/10 版本的 adoptopenjdk 镜像:
adoptopenjdk/openjdk8
adoptopenjdk/openjdk9
adoptopenjdk/openjdk10
之后是 OpenJ9,也是由 adoptopenjdk 为 8/9 版本所提供的,同时包含为 9 发布的每日构建版(相关内容请查看我之前的博文):
adoptopenjdk/openjdk8-openj9
adoptopenjdk/openjdk9-openj9
adoptopenjdk/openjdk9-openj9:nightly
同时我决定也引入 IBM 自家的 J9 镜像:
ibmcom/ibmjava:8-jre
在构建我的 Spring Boot 应用之后,我使用下面命令启动了每个 Docker 镜像:
docker run -it -v /Projects/temp/spring-boot-example:/app/spring-boot-example -p 8080:8080 IMAGE_NAME /bin/bash
我将 "spring-boot-example" 项目目录映射到了 "/apps/spring-boot-example",然后我就可以启动容器中的 JAR 文件。同时我将端口 8080 转发到我的主机,这样我就可以调用这些端点。
下一步,在容器内部,我启动了 Spring Boot 应用:
java -jar /app/spring-boot-example/target/spring-boot-example-0.0.1-SNAPSHOT.jar
等待一段时间,调用几次端点,并执行一个 GC,之后我测量内存使用情况。
然后,我调用了包含数组排序测试的 "/loadtest" 端点,然后等待结果.
下面是示例的 Spring Boot 应用的内存使用结果:
首先你可以看到 Java 8 的内存使用明显高于 Java 9 和 10。
但是令人最吃惊的是 OpenJ9 和 J9 使用的内存减少的量,如果将 Java8 和 OpenJ9 对比,后者几乎是前者的 4 倍。我很惊奇,这是怎么形成的?现在我们几乎可以将 Spring Boot 服务称为微服务 (micro) 了!
我也尝试过运行一些生产环境中的 Spring Boot 代码(不仅仅是简单的示例),通过它们我看到了内存使用量减少了 40-50% 的改进。
我已经了解到,如果您从 CPU 密集任务方面去对比,OpenJ9 不如 HotSpot。这就是为什么我为此创建了一个小测试。
给具有 1000000 个随机 long 值的 1000 个数组排序。这大约需要花费 100 秒,这应该给了 JVM 足够的时间去调整和优化。我已经为每个测试镜像调用了两次基准测试。我记录了第二次的时间以尝试去排除热启动的时间。
从图表中我们能看到 J9 和 OpenJ9 的镜像确实很慢,最快不超过 18%。从这个特定测试用例看来,Java 8 击败了绝大多数 Java 9 的实现(除了 OpenJ9 以外)。
在生产环境中,与内存问题相比,我当前的项目存在更多的 CPU 问题(频繁运行过程中的内存溢出,而 CPU 使用率是 1-2%)。在不久的将来,我们肯定会切换至使用 OpenJ9。
测试期间,我们也确实遇到了一些问题:
Hessian:(二进制协议)有一个内置的假设,即 System.identityHashCode 总是返回一个正数。对于 HotSpot 来说这是个事实,但是 OpenJ9/J9 也可以返回负数。这是个公开的问题,Hessian 项目在近几年内并没有解决这个问题,似乎它已经停止维护了?所以我们的解决方案是远离 Hessian。
Instana:我们热爱我们的监控工具 Instana,但是它连接代理到 OpenJ9/J9 的时候出了些问题。幸运的是,Instana 的开发者帮助我们确认了一个错误,还进行了修复(并且会自动更新,哇!)
我没有注意到的开放性问题:
你仍然可以在 OpenJ9 中对信息进行 get/use jmap/hprof 等操作吗?
它将如何在更长久的生产环境中保持运行?
我们会发现其他的奇怪错误吗?感觉很棘手…
你尝试过使用 OpenJ9/J9 吗?在评论区中和大家分享你的看法吧!