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

  • Post author:
  • Post category:其他


什么是OpenJ9


OpenJ9

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

为什么需要OpenJ9


HotSpot JVM

在Java虚拟机领域独领风骚多年了,但是近年来有

GraalVM



OpenJ9

等等后起之秀崭露头角,开始在各自的领域发力。

正如

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加载类时,会分别生成

RAMClass



ROMClass

并存储在本地的内存中。如果开启了类共享,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

被分为了两个部分:

allocate



survivor

。GC过程如下图所示:

  1. 新对象进入

    nursery



    allocate

    区域

  2. allocate

    渐渐增长直至完全充满
  3. 本地清扫程序启动,将所有

    可达

    的对象放入到

    survivor

    ,或者如果对象已经到达

    tenure age

    ,则直接进入

    tenure

    区域
  4. 之后

    allocate



    survivor

    角色互换,先前的

    allocate

    变为

    survivor

    ,先前的

    survivor

    则变为

    allocate

    ,为下一次GC作准备


allocate



survivor

的相对大小会根据一种叫做

tilting

的动态调整技术来进行变化。刚开始

allocate



survivor

的大小是五五开的,在清理过程中如果发现哪一边所需的空间较小,会对空间进行动态调整以满足GC的需求。以此可以尽可能减少GC的周期。

其中

tenure age

是指对象在

allocate



survivor

的切换过程中存活下来的次数,JVM会依据此数据来决定对象是否转移到

tenure

。可以通过

-Xgc:scvTenureAge=<n>

参数来设置初始的

tenure age

,后续的

tenure age

可能会随着GC的进程由JVM进行自适应来优化当前的空间使用率。当然如果要关闭

tenure age

自适应,可以使用此参数

-Xgc:scvNoAdaptiveTenure


tenure

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

-Xnoloa

参数。

balanced


balanced

GC策略使用参数

-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的是

eden



age

为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-64



AIX平台

。此策略是一种具备较短暂停时间的增量的,确定的垃圾回收策略。


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

也做了支持,完全可以不做变动。

等价参数

以下是在

HotSpot



OpenJ9

中等价的参数

HotSpot OpenJ9
-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

独占鳌头,而是各大不同的虚拟机各领风骚的时代。让我们不断关注吧!



版权声明:本文为m0_71777195原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。