写点什么

有了 HotSpot JVM 为什么还需要 OpenJ9?

作者:骑牛上青山
  • 2023-02-04
    上海
  • 本文字数:4628 字

    阅读完需:约 15 分钟

什么是 OpenJ9

OpenJ9是一个致力于构建更小内存使用,更快启动速度和更高吞吐量的独立实现的 Java 虚拟机。项目由 IBM 发起,并在之后开源并捐赠给 Eclipse 基金会。

为什么需要 OpenJ9

HotSpot JVM在 Java 虚拟机领域独领风骚多年了,但是近年来有GraalVMOpenJ9等等后起之秀崭露头角,开始在各自的领域发力。


正如OpenJ9自己的介绍一样:


A Java Virtual Machine for OpenJDK that's optimized for small footprint, fast start-up, and high throughput


OpenJ9的特点就是性能:低内存占用,快速启动,高吞吐。我们就来看看为了实现这些能力OpenJ9都做了什么,然后回过头再来看他是否能够在某些场合替代HotSpot JVM

性能

从官网上截取了官方对于OpenJ9的性能对比。可以看到无论是 jdk11 还是 jdk8,OpenJ9在启动时间和内存占用上都占有较大优势。




类共享

OpenJ9的一大特点就是类共享。共享类无需用户进行特殊处理,JVM 会自行进行处理来优化内存占用和改进启动时间。在 OpenJ9 的实现中,所有的系统类,应用类和 AOT 预编译的代码都能被存在共享内存的动态类缓存中。类共享对于多个运行相同代码的 JVM 将是巨大的优化,因此在当前的云原生的蓬勃发展下OpenJ9是一个非常有诱惑力的选择。

类共享使用

想要开启使用类共享很简单,只要在 JVM 启动项中添加-Xshareclasses[:name=<cachename>]即可,JVM 会自行构建缓存。

类共享原理

共享类缓存

共享类缓存(SCC, shared classes cache)是一个固定大小的共享内存区域。除非配置了不持久化,否则 SCC 数据即使在 JVM 重启后也会依然存在。


OpenJ9的共享缓存不属于某个 JVM,各个 JVM 之间也不会有主次之分,但是所有的 JVM 都能够对共享缓存进行读写。

类缓存使用

一般的 JVM 在装载类的时候遵循如下的流程:



使用类共享的情况下类的加载机制会发生变化:



启用类共享的情况下,在父类加载器层层加载都没法获取类时会去共享缓存查询类,然后才会尝试去文件系统获取。


java.net.URLClassLoader (在 Java9+ jdk.internal.loader.BuiltinClassLoader)已经集成了共享类缓存的 API,因此所有继承java.net.URLClassLoader的类加载器都能够使用共享类缓存。如果是自定义的类加载器,可以使用OpenJ9提供的 API。


OpenJ9的实现中,Java 类被分为了两部分:


  • ROMClass 只读,存储的是类的不可变数据

  • RAMClass 可写,存储的是类的可变数据,例如静态类变量


虽然RAMClass指向了ROMClass,但是这两者是完全分开的。因此在不同的 JVM 之间分享ROMClass以及在同一个 JVM 使用RAMClass是很安全的。在未开启类共享的情况下,当 JVM 加载类时,会分别生成RAMClassROMClass并存储在本地的内存中。如果开启了类共享,JVM 加载类时发现共享内存中已经存在了该类,那么就只需要创建RAMClass然后存放在本地内存使用即可。


AOT编译后的代码也会被存储在共享缓存中。当启用共享类缓存时,AOT会将将 Java 类编译成本机代码,以便同一程序后续使用。

文件系统变化导致的类缓存问题

因为共享缓存是没有过期时间的,因此可能会存在类文件产生变动导致的缓存失效。因此 JVM 需要处理这种情况下的类缓存的更新问题。JVM 需要保证类加载器获取的类必须和文件系统中的类是一致的。


JVM 通过将时间戳值存储到缓存中并将缓存值与实际值进行比较来检测文件系统更新。在类发生更新的情况下这些操作对于类加载是透明的,因此用户对于类进行修改操作都很容易被感知到并且进行相应的处理。

缓存版本差异

在某些情况下,从一个版本的 JVM 创建的缓存可能与从不同版本创建的缓存不兼容。遇到这种情况即使两个缓存名称相同,JVM 也会依然创建一个新缓存,同时通过共享类缓存的世代号(generation number)来检测冲突。

redefine 和 retransform 类

类缓存机制听上去很合理,但是特殊情况下会有些不一样,比如当你使用了 Java Agent 时,会有一些类会被redefined或者retransformed。针对这两种情况,OpenJ9做了不同的处理:


  • redefined redefine 会替换字节码,因此这种类不会被存放入缓存中

  • retransformed retransform 会修改字节码,并且有可能会进行多次的修改,这种类默认不会被存入缓存,但是可以通过-Xshareclasses:cacheRetransformed选项来开启

AOT

AOT 通过将 java 类编译成native code并缓存到共享数据缓存中。后续虚拟机可以从共享数据缓存加载和使用 AOT 的代码,而不会导致性能下降。


如果要关闭,可以使用-Xnoaot参数进行配置

内存管理

GC 策略

OpenJ9提供了一系列 GC 的策略用于不同场合的内存管理。

gencon

gencon(Generational Concurrent GC)是OpenJ9默认的 GC 策略,使用-Xgcpolicy:gencon进行配置。这个 GC 策略适用于大多数的应用,尤其是有许多生命周期很短的对象的事务性应用。此策略旨在不影响吞吐量的情况下减少 GC 暂停次数。


此策略类似于HotSpot JVM的分代收集策略,只是OpenJ9会在一些细节上有一些不同。


gencon策略中,Java 堆被分成了两部分:


  • nursery 存储新创建的对象

  • tenure 存储达到tenure age的对象


nursery被分为了两个部分:allocatesurvivor。GC 过程如下图所示:



  1. 新对象进入nurseryallocate区域

  2. allocate渐渐增长直至完全充满

  3. 本地清扫程序启动,将所有可达的对象放入到survivor,或者如果对象已经到达tenure age,则直接进入tenure区域

  4. 之后allocatesurvivor角色互换,先前的allocate变为survivor,先前的survivor则变为allocate,为下一次 GC 作准备


allocatesurvivor的相对大小会根据一种叫做tilting的动态调整技术来进行变化。刚开始allocatesurvivor的大小是五五开的,在清理过程中如果发现哪一边所需的空间较小,会对空间进行动态调整以满足 GC 的需求。以此可以尽可能减少 GC 的周期。


其中tenure age是指对象在allocatesurvivor的切换过程中存活下来的次数,JVM 会依据此数据来决定对象是否转移到tenure。可以通过-Xgc:scvTenureAge=<n>参数来设置初始的tenure age,后续的tenure age可能会随着 GC 的进程由 JVM 进行自适应来优化当前的空间使用率。当然如果要关闭tenure age自适应,可以使用此参数-Xgc:scvNoAdaptiveTenure


tenure默认会被分为两部分:小对象区域(SOA),大对象区域(LOA),SOA 中存放不大于 64KB 的对象,LOA 则相反。如果要禁用 LOA,可以使用-Xnoloa参数。

balanced

balancedGC 策略使用参数-Xgcpolicy:balanced启用(需注意此策略仅支持 64 位平台)。在此策略下 Java 堆被分为一个个不同的region(1024 - 2048),这些region由增量分代收集器单独管理,以减少大堆上的最大暂停时间并提高垃圾回收的效率。此策略将堆进行切分以避免全局的垃圾回收,以此来减少垃圾回收时的长暂停。


balanced策略类似于HotSpot中的 G1 收集器。


在虚拟机启动的时候,堆内存会被划分为大小相等的region,这些region就是balanced gc 策略的基本单元。


region存在如下特点:


  1. 由于region的特殊性,在一开始就强制限定了对象的最大大小。

  2. 对象始终被分配在单个region内,不会跨region分配。

  3. region大小始终是 2 的 N 次幂,且是在启动时根据堆的最大值来决定的。

  4. 虚拟机总是会生成 1024~2048 个region


基于上述特性我们来看下balanced gc 策略的 gc 流程。



上图是堆上的region的划分。其中age为 0 的是edenage为 24 是old,中间的region则分布着 1-23 的age


在进行垃圾回收时eden区总是会参与其中,而old只在少数情况下会被加入其中。当进行过一次垃圾回收后,age为 N 的幸存者会被放入到age为 N+1 的区域中。然后随着时间的推移,可用的幸存区域会变得越来越少,之后到了某个时间节点就需要进行全局标记清理整个堆。


大多数的对象可以很轻松的存放入region中,但是也有少部分的大对象没法正常存储在region中,因此提供了Arraylets来处理当前情况。

Arraylets


Arraylets是用来解决大对象无法在单个region中存储的问题的。Arraylets会有一个结构Spine,其中存放着类指针和大小,其中还包含Arrayoids指向各个叶子结点。以此可以将大对象进行切分,存储到不同的region中。

optavgpause

optavgpause(optimize for pause time)策略使用参数-Xgcpolicy:optavgpause来启用。此策略可以减少 GC 暂停时间,但是会牺牲部分吞吐量。


optavgpause策略使用平面的 Java 堆。全局 GC 进行循环并发mark-sweep标记清除操作。由于其全局并发处理的特性,会显著减少 GC 暂停时间,但是会大大影响吞吐量。

optthruput

optthruput(optimize for throughput)策略使用参数-Xgcpolicy:optthruput来启用。此策略和optavgpause策略有着类似的设计,只是此策略专注于吞吐量的优化,因此虽然提升了吞吐量,但是会有较高的 GC 暂停时间。


optthruput策略使用平面的 Java 堆。全局 GC 使用mark-sweep进行循环标记清除操作。由于不是并发清理,因此需要对堆进行独占访问,导致应用程序线程在操作发生时停止。因此,可能会出现长时间的 GC 停顿。

metronome

metronome策略使用参数-Xgcpolicy:metronome来启用,其只支持linux x86-64AIX平台。此策略是一种具备较短暂停时间的增量的,确定的垃圾回收策略。


metronome策略会在堆上分配连续的范围,将这些划分为大小相等的区域,通常为 64Kb。其中每个区域中只存放大小相等的对象或者是arraylet。这种形式简化了对象分配和空间合并的,以此保证 GC 的吞吐量。

如何选择合适的 GC 策略

如何使用 OpenJ9

如果之前是在使用HotSpot JVM想要尝试一下OpenJ9,那么可以参考本章节的建议。


目前OpenJ9支持 jdk8,jdk11 和 jdk17。由于OpenJ9遵循了虚拟机规范,因此在大部分的场景下不需要过多的变动。

启动项

要想尝试OpenJ9,那么首先需要考虑到的是其启动项和其他虚拟机的不同之处。不过OpenJ9在这方面做了兼容,绝大部分的HotSpot JVM启动项都能够在OpenJ9中直接使用,除了少部分。

堆参数

OpenJ9中所有涉及到堆的设置的参数都是需要注意的,这些参数名称虽然和HotSpot JVM一样,但是其包含的意义会有所不同,因为两者的 GC 策略会有不同之处。但是可以简单的将 GC 策略gencon理解为分代收集,balanced理解为 G1,配置就大同小异了。可以参考这些链接:xmn xms


这里会有一个不同之处,OpenJ9可以通过设置xmo来设置gencon中的tenure的值。

dump

OpenJ9中提供了-Xdump参数,用于进行 JVM 的诊断,此参数用于替代-XX:HeapDumpPath-XX:+HeapDumpOnOutOfMemory等参数,功能更加强大。当然旧的这些dump参数OpenJ9也做了支持,完全可以不做变动。

等价参数

以下是在HotSpotOpenJ9中等价的参数


GC 策略

详情可以参照上文的 GC 章节


大致上来说使用默认的 GC 策略即可,配置也可以使用默认配置。

云原生支持

OpenJ9提供了-Xtune:virtualized参数来用于云原生的环境,此设置可以在云原生环境下以牺牲少量的吞吐量为代价来节省 cpu 资源。

k8s

在 k8s 场景下,如果想要使用共享类缓存的话需要为 pod 创建共享存储卷,来打通不同的 pod 之间的共享机制。

总结

OpenJ9主打的是节约资源与快速启动。而在微服务和云原生广泛应用的当下,节约资源正是切合了当下很多企业降本增效的想法。如果大家有兴趣的话,建议可以尝试下使用OpenJ9


在新技术与新概念层出不穷的当下,我们面临的环境与挑战也与以往有了不同,因此有了一些针对不同场合,为了解决不同问题的JVM应运而生,或许在不久的将来,就不再会是HotSpot独占鳌头,而是各大不同的虚拟机各领风骚的时代。让我们不断关注吧!


参考资料


[1] https://developer.ibm.com/articles/garbage-collection-tradeoffs-and-tuning-with-openj9/


[2] https://developer.ibm.com/tutorials/j-class-sharing-openj9/


[3] https://www.eclipse.org/openj9/docs/aot/


[4] https://www.eclipse.org/openj9/docs/gc/


[5] https://www.eclipse.org/openj9/docs/shrc/


[6] https://blog.openj9.org/2019/05/01/double-map-arraylets/


发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2021-05-18 加入

还未添加个人简介

评论

发布
暂无评论
有了HotSpot JVM为什么还需要OpenJ9?_Java_骑牛上青山_InfoQ写作社区