当前位置: 首页 > 后端技术 > Java

使用HotSpotJVM为什么需要OpenJ9?

时间:2023-04-02 01:12:38 Java

什么是OpenJ9OpenJ9是Java虚拟机的独立实现,致力于构建更小的内存占用、更快的启动速度和更高的吞吐量。该项目由IBM发起,后来开源并捐赠给了EclipseFoundation。为什么需要OpenJ9?HotSpotJVM多年来一直是Java虚拟机领域的佼佼者,但近几年GraalVM、OpenJ9等后起之秀纷纷涌现,开始在各自领域发力。正如OpenJ9自己的介绍:AJavaVirtualMachineforOpenJDKthat'soptimizedforsmallfootprint,faststart-up,andhighthroughputOpenJ9的特点是性能:低内存占用,快速启动,高吞吐量。我们先看看OpenJ9为了实现这些能力做了什么,回过头来看看他能不能在某些场合替代HotSpotJVM。性能OpenJ9官方性能对比截取自官网。可以看出无论是jdk11还是jdk8,OpenJ9在启动时间和内存占用上都有很大的优势。类共享OpenJ9的一个主要特性是类共享。共享类不需要用户进行特殊处理,JVM会自行处理以优化内存使用并改善启动时间。在OpenJ9的实现中,所有的系统类、应用类和AOT预编译代码都可以存放在共享内存的动态类缓存中。类共享对于运行相同代码的多个JVM来说将是一个巨大的优化,所以OpenJ9在当前云原生蓬勃发展的情况下是一个非常有诱惑力的选择。类共享启用类共享非常简单,只需在JVM启动项中添加-Xshareclasses[:name=],JVM会自行构建缓存。类共享原理共享类缓存共享类缓存(SCC,sharedclassescache)是一块固定大小的共享内存区域。除非配置了非持久化,否则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仍然会创建一个新的缓存,同时通过共享类缓存的世代号来检测冲突。redefine和retransform类缓存机制听上去很合理,但在特殊情况下还是有些区别的。例如,当你使用JavaAgent时,一些类将被重新定义或重新转换。对于这两种情况,OpenJ9做了不同的处理:redefinedredefine会替换字节码,所以这个类不会存入缓存retransformedretransform会修改字节码,可能会进行多次修改,也就是类默认不缓存,但是AOTAOT可以通过使用-Xshareclasses:cacheRetransformed选项来启用,将java类编译成本机代码并将它们缓存在共享数据缓存中。后续VM可以从共享数据缓存中加载和使用AOT的代码,而不会降低性能。如果要关闭,可以使用-Xnoaot参数配置内存管理GC策略OpenJ9提供了一系列GC策略,用于不同场合的内存管理。gencongencon(GenerationalConcurrentGC)是OpenJ9默认的GC策略,使用-Xgcpolicy:gencon配置。这种GC策略适用于大多数应用程序,尤其是具有许多短生命周期对象的事务性应用程序。该策略旨在减少GC暂停次数而不影响吞吐量。这种策略类似于HotSpotJVM的分代收集策略,但是OpenJ9在一些细节上会有一些不同。在gencon策略中,Java堆分为两部分:nursery存放新创建的对象tenure和存放达到tenureage的对象。Nursery分为两部分:allocate和survivor。GC过程如下图所示:新对象进入nursery的allocate区,allocate逐渐增长,直到完全填满。本地清理程序启动,将所有可达的对象放入survivor中,或者如果对象已经达到tenure年龄,则直接进入tenure区域。allocate和survivor的角色可以互换。之前的allocate变成survivor,之前的survivor变成allocate。在为下一次GC做准备时,allocate和survivor的相对大小将根据称为倾斜的动态调整技术进行更改。一开始allocate和survivor的大小是50-50。如果在清理过程中发现哪一侧需要的空间较少,就会动态调整空间以满足GC的需要。这样可以尽可能的减少GC的周期。其中,tenureage是指对象在allocate和survivor之间切换期间存活的次数,JVM会根据这个数据决定是否将对象转移到tenure。可以通过-Xgc:scvTenureAge=参数设置初始的tenureage,随后的tenureage可能会随着GC的进行而由JVM调整以优化当前的空间使用。当然,如果你想关闭tenureageadaptation,你可以使用这个参数-Xgc:scvNoAdaptiveTenure。Tenure默认分为两部分:小对象区(SOA)和大对象区(LOA)。SOA存储的对象不大于64KB,而LOA则相反。如果要禁用LOA,可以使用-Xnoloa参数。使用参数-Xgcpolicy:balanced启用balancedGC策略(注意该策略仅支持64位平台)。在这种策略下,Java堆被划分为不同的区域(1024-2048),由增量分代收集器分别管理,以减少大堆上的最大停顿时间,提高垃圾收集效率。此策略拆分堆以避免全局垃圾收集,从而减少垃圾收集期间的长时间停顿。平衡策略类似于HotSpot中的G1收集器。虚拟机启动时,堆内存会被分成大小相等的区域,这些区域是平衡gc策略的基本单元。区域具有以下特点:由于区域的特殊性,对象的最大尺寸从一开始就被强制限制。对象始终在单个区域内分配,而不是跨区域分配。区域大小始终为2的N次方,并在启动时根据堆的最大值确定。虚拟机总会生成1024~2048个区域。基于以上特点,我们来看看平衡gc策略的gc过程。上图展示了堆上region的划分。其中年龄为0为eden,年龄为24为old,中间区域分布为1-23岁。eden区总是参与垃圾回收,而old区只是在少数情况下加入。垃圾回收后,年龄为N的幸存者将被放置在年龄为N+1的区域中。然后随着时间的推移,可用的存活区会越来越少,然后在某个时间节点,需要进行全局标记,清理整个堆。大多数对象可以很容易地存储在区域中,但也有少量大对象无法正常存储在区域中,因此提供了Arraylets来处理当前情况。ArrayletsArraylets用于解决单个region无法存储大对象的问题。Arraylets会有一个结构体Spine,里面存放了类指针和大小,同时也包含了指向各个叶节点的Arrayoids。这样就可以将大的对象划分到不同的区域存储。optavgpauseoptavgpause(优化暂停时间)策略通过参数-Xgcpolicy:optavgpause启用。这种策略可以减少GC暂停时间,但是会以牺牲一些吞吐量为代价。optavgpause策略使用平面Java堆。globalGC执行循环和并发的mark-sweep标记清除操作。由于其全局并发处理的特性,会显着减少GC停顿时间,但会极大地影响吞吐量。使用参数-Xgcpolicy:optthruput启用optthruputoptthruput(优化吞吐量)策略。该策略与optavgpause策略有相似的设计,只是该策略侧重于吞吐量优化,因此虽然吞吐量有所提升,但GC暂停时间会更高。optthruput策略使用平面Java堆。全局GC使用标记-清除进行循环标记清除操作。由于清理不是并发的,它需要对堆进行独占访问,导致应用程序线程在操作发生时停止。因此,可能会出现长时间的GC停顿。使用参数-Xgcpolicy:metronome启用节拍器策略,仅支持linuxx86-64和AIX平台。此策略是一种增量的、确定性的垃圾回收策略,暂停时间较短。节拍器策略在堆上分配连续的范围,将它们划分为大小相等的区域,通常为64Kb。每个区域中只存储相同大小的对象或数组。这种形式简化了对象分配和空间合并,以确保GC吞吐量。如何选择合适的GC策略GC策略适合场景。gencon默认的策略是分代收集,性能优异。它适用于大多数场合。具有相对统一的对象生命周期的应用程序,即当大量对象一起出生和死亡时。节拍器专为需要精确收集暂停时间上限和指定应用程序利用率的应用程序而设计。如何使用OpenJ9如果您想在尝试OpenJ9之前使用HotSpotJVM,那么您可以参考本节中的建议。目前OpenJ9支持jdk8、jdk11和jdk17。由于OpenJ9遵循虚拟机规范,所以在大多数场景下不需要做太多改动。启动项如果你想尝试OpenJ9,首先要考虑的是它的启动项和其他虚拟机的区别。不过OpenJ9在这方面做了兼容。大多数HotSpotJVM启动项都可以在OpenJ9中直接使用,除了少数几个。堆参数OpenJ9中与堆设置相关的所有参数都需要注意。虽然这些参数的名字和HotSpotJVM的一样,但是它们的含义会有所不同,因为两者的GC策略会不同。但是你可以简单理解GC策略gencon为分代收集,balanced为G1,配置类似。可以参考这些链接:xmnxms这里会有区别,OpenJ9可以通过设置xmo来设置gencon中tenure的值。dump在OpenJ9中提供了-Xdump参数用于JVM诊断。该参数用于替代-XX:HeapDumpPath和-XX:+HeapDumpOnOutOfMemory等参数,功能更强大。当然,这些旧的转储参数也被OpenJ9支持,因此无需更改它们。等效参数以下是HotSpot和OpenJ9中的等效参数。HotSpotOpenJ9-Xcomp-Xjit:count=0-Xgc-Xgcpolicy-XX:+UseNUMA-Xnuma:noneGC策略详情可以参考上面的GC章节。一般来说,使用默认的GC策略就够了,配置也可以使用默认的配置。云原生支持OpenJ9为云原生环境提供-Xtune:virtualized参数。此设置可以在云原生环境中以少量吞吐量为代价来节省CPU资源。在k8s场景下,如果要使用共享类缓存,需要为pod创建共享存储卷,打通不同pod之间的共享机制。总结OpenJ9侧重于节省资源和快速上手。在微服务和云原生被广泛应用的当下,节约资源恰恰符合很多企业降本增效的想法。如果你有兴趣,我建议你尝试使用OpenJ9。随着新技术、新理念的出现,我们面临的环境和挑战也与以往不同。因此,一些JVM应运而生,针对不同的场合,解决不同的问题。或许在不久的将来,不再是HotSpot独领风骚的时代,而是不同的虚拟机独领风骚。让我们拭目以待!参考文献[1]https://developer.ibm.com/art...[2]https://developer.ibm.com/tut...[3]https://www.eclipse.org/openj...[4]https://www.eclipse.org/openj...[5]https://www.eclipse.org/openj...[6]https://blog.openj9.org/2019/...