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

什么是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策略

GC策略适合场景
gencon默认策略,分代收集,性能优秀,适合大部分场合
balanced比gencon更适合处理大对象,更适合对GC暂停时间有较高要求的场合
optavgpause和optthruput适合对象生命周期比较统一的应用,即对象大量一起生一起死的场合
metronome专为需要精确的收集暂停时间上限以及指定应用程序利用率的应用程序而设计

如何使用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中等价的参数

HotSpotOpenJ9
-Xcomp-Xjit:count=0
-Xgc-Xgcpolicy
-XX:+UseNUMA-Xnuma:none

GC策略

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

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

云原生支持

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

k8s

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

总结

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

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

作者:骑牛上青山
链接:https://juejin.cn/post/7196059552122617912
来源:稀土掘金

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值