Class Sharing in Eclipse OpenJ9: How to Improve Memory, Performance (Part 1)
内存占用量和启动时间是Java虚拟机(JVM)的重要性能指标。 在云环境中,内存占用变得尤为重要,因为您需要为应用程序使用的内存付费。 在本教程中,我们将向您展示如何使用Eclipse OpenJ9中的共享类功能来减少内存占用并改善JVM启动时间。
在2017年,IBM开源了J9 JVM,并将其贡献给Eclipse基金会,在那里它成为Eclipse OpenJ9项目。 从Java 5开始,J9 JVM已支持从系统类到应用程序类的类共享超过10年。
在OpenJ9实现中,所有系统,应用程序类和提前(AOT)编译代码都可以存储在共享内存中的动态类缓存中。 这些共享类功能在OpenJ9支持的所有平台上实现。 该功能甚至支持与运行时字节码修改集成,我们将在本文的第2部分中稍后讨论。
共享类功能是启动后无需考虑的功能,但是它提供了一个强大的作用域,可以减少内存占用并缩短JVM启动时间。 因此,它最适合于多个JVM运行相似代码或定期重新启动JVM的环境。
除了JVM及其类加载器中的运行时类共享支持外,还提供了一个公共Helper API,用于将类共享支持集成到自定义类加载器中。 <!-[如果!supportAnnotations]-> <!-[如果!supportNestedAnchors]-> <!-[endif]->
您可以从Adopt OpenJDK项目中下载带有OpenJ9的JDK,也可以从Docker映像中将其拉出(如果您想按照示例进行操作)。
工作原理
让我们从探讨共享类功能的运行方式的技术细节开始。
启用课程共享
要启用类共享,请将
您可以使用参数
共享类缓存
共享类高速缓存由固定大小的共享内存组成,该内存在JVM的生命周期或系统重新启动后将一直存在,除非使用了非持久性共享高速缓存。 系统上可以存在任何数量的共享缓存,并且所有缓存均受操作系统设置和限制的约束。
没有JVM拥有共享缓存,也没有主/从JVM概念。 相反,任意数量的JVM可以同时读取和写入共享缓存。
共享缓存的大小不能增加。 当它变满时,JVM仍然可以从中加载类,但是它不再可以在其中存储任何数据。 您可以先创建一个大型的共享类高速缓存,同时为可以使用多少共享高速缓存空间设置一个软的最大限制。 当您想将更多数据存储到共享缓存中而不关闭与之连接的JVM时,可以增加此限制。 请查看OpenJ9文档,以获取有关软最大限制的更多详细信息。
此外,还有几种JVM实用程序可管理主动共享的缓存。 我们将在下面的"共享类实用程序"部分中讨论这些内容。
使用JVM命令行显式销毁共享缓存时,会将其删除。
如何缓存类?
当JVM加载一个类时,它首先在类加载器高速缓存中查找以查看其所需的类是否已经存在。 如果是,它将从类加载器缓存中返回该类。 否则,它将从文件系统加载类,并将其作为
类加载器缓存
父母
文件系统
相反,运行具有类共享功能的JVM使用以下顺序:
类加载器缓存
父母
共享类缓存
文件系统
使用公共Helper API从共享类缓存中读取和写入类。 Helper API已集成到
什么是缓存的?
共享类缓存可以包含引导程序和应用程序类,描述这些类的元数据以及提前(AOT)编译代码。
在OpenJ9实现中,Java类分为两部分:
只读部分,称为ROMClass,其中包含该类的所有不可变数据
包含可变数据(例如静态类变量)的RAMClass
RAMClass指向其ROMClass中的数据,但这两个是完全分开的。 因此,在JVM之间以及同一JVM中的RAMClass之间共享ROMClass是非常安全的。
在非共享情况下,当JVM加载一个类时,它会分别创建ROMClass和RAMClass并将它们都存储在本地进程内存中。 在共享的情况下,如果JVM在共享的类高速缓存中找到ROMClass,则仅需要在其本地内存中创建RAMClass;否则,JVM将不会在ROMClass中创建ROMClass。 然后,RAMClass引用共享的ROMClass。
由于大多数类数据存储在ROMClass中,因此可以节省内存(请参见"内存占用<!-[如果!supportAnnotations]->"中的详细讨论)。 填充缓存后,JVM的启动时间也大大缩短,因为定义每个缓存类的工作已经完成,并且这些类是从内存而不是从文件系统加载的。 <!-[如果!supportAnnotations]->填充新的共享缓存的启动时间开销并不重要,因为每个类仅需要按照定义的方式重新放置到共享缓存中。
AOT编译的代码也存储在共享缓存中。 启用共享类缓存后,将自动激活AOT编译器。 AOT编译允许将Java类编译成本机代码,以供以后执行同一程序。 当应用程序运行并将所有生成的AOT代码缓存在共享类缓存中时,AOT编译器会动态生成本地代码。 通常,AOT编译代码的执行速度比解释的字节码快,但不及JIT编码的代码快。 执行该方法的后续JVM可以从共享缓存中加载和使用AOT代码,而不会导致生成JIT编译的代码时性能下降,从而缩短了启动时间。 创建新的共享缓存时,可以使用选项
如果文件系统上的类发生更改会发生什么?
由于共享类高速缓存可以无限期地保留,因此可能会发生文件系统更新,这些更新会使共享高速缓存中的类和AOT代码无效。 如果类加载器请求共享类,则返回的类应始终与从文件系统加载的类相同。 这在加载类时透明地发生,因此,在知道始终加载正确的类的情况下,用户可以在共享类缓存的生存期内修改和更新任意数量的类。
类更改的陷阱:示例
想象一下由JVM存储到共享缓存中的类C1。 然后,当JVM关闭时,将更改C1并重新编译。 JVM重新启动时,不应加载C1的缓存版本。
类似地,想象一下以
JVM通过将时间戳记值存储到共享缓存中并在每次类加载时将缓存的值与实际值进行比较来检测文件系统更新。 如果检测到JAR文件已更新,则不知道已更改了哪些类。 因此,所有类以及来自缓存中该JAR的AOT代码都会立即标记为过期,并且无法从缓存中加载。 当从文件系统中加载该JAR中的类并将其重新添加到缓存时,仅完整地添加已更改的类;否则,将仅添加已更改的类。 那些没有改变的东西实际上已经过时了。
无法从共享类高速缓存中清除类,但是JVM会尝试最有效地利用其拥有的空间。 例如,即使从许多不同的位置加载同一类,也永远不会添加两次。 因此,如果通过三个不同的JVM从
共享类实用程序
您可以使用多种实用程序来管理共享类缓存,所有实用程序都是
为了演示这些选项的用法,让我们来看一些示例。
首先,让我们通过运行具有不同缓存名称的Hello类来创建两个共享缓存,如清单1所示:
列出1.创建两个共享缓存
1 2 3 4 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -cp . -Xshareclasses:name=Cache1 Hello Hello C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -cp . -Xshareclasses:name=Cache2 Hello Hello |
运行
列出2.列出所有共享的缓存
1 2 3 4 5 6 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:listAllCaches Listing all caches in cacheDir C:\Users\Hang Shao\AppData\Local\javasharedresources\ Cache name level cache-type feature last detach time Compatible shared caches Cache1 Java8 64-bit persistent cr Mon Apr 23 15:48:12 2018 Cache2 Java8 64-bit persistent cr Mon Apr 23 15:49:46 2018 |
运行
清单3.共享缓存的摘要统计信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,printStats Current statistics for cache"Cache1": Cache created with: -Xnolinenumbers = false BCI Enabled = true Restrict Classpaths = false Feature = cr Cache contains only classes with line numbers base address = 0x000000001214C000 end address = 0x0000000013130000 allocation pointer = 0x0000000012297DB8 cache size = 16776608 softmx bytes = 16776608 free bytes = 13049592 ROMClass bytes = 1359288 AOT bytes = 72 Reserved space for AOT bytes = -1 Maximum space for AOT bytes = -1 JIT data bytes = 1056 Reserved space for JIT data bytes = -1 Maximum space for JIT data bytes = -1 Zip cache bytes = 902472 Data bytes = 114080 Metadata bytes = 18848 Metadata % used = 0% Class debug area size = 1331200 Class debug area used bytes = 132152 Class debug area % used = 9% # ROMClasses = 461 # AOT Methods = 0 # Classpaths = 2 # URLs = 0 # Tokens = 0 # Zip caches = 5 # Stale classes = 0 % Stale classes = 0% Cache is 22% full Cache is accessible to current user = true |
还有其他
清单4.列出共享缓存的类路径内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,printStats=classpath Current statistics for cache"Cache1": 1: 0x000000001360E3FC CLASSPATH C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\bin\compressedrefs\jclSC180\vm.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\se-service.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib t.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib esources.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\jsse.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\charsets.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\jce.jar C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\tools.jar 1: 0x000000001360A144 CLASSPATH C:\OpenJ9 … … |
共享缓存使用
清单5.销毁缓存
1 2 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,destroy JVMSHRC806I Compressed references persistent shared cache"Cache1" has been destroyed. Use option -Xnocompressedrefs if you want to destroy a non-compressed references cache. |
清单6中所示的
清单6.销毁一周内未使用的缓存
1 2 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,expire=10080 Hello Hello |
详细选项
详细选项提供有关类共享正在做什么的有用反馈。 它们都是
清单7中所示的
清单7.获取JVM状态信息
1 2 3 4 5 6 7 8 9 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,verbose Hello [-Xshareclasses persistent cache enabled] [-Xshareclasses verbose output enabled] JVMSHRC236I Created shared classes persistent cache Cache1 JVMSHRC246I Attached shared classes persistent cache Cache1 JVMSHRC765I Memory page protection on runtime data, string read-write data and partially filled pages is successfully enabled Hello JVMSHRC168I Total shared class bytes read=11088. Total bytes stored=2416962 JVMSHRC818I Total unstored bytes due to the setting of shared cache soft max is 0. Unstored AOT bytes due to the setting of -Xscmaxaot is 0. Unstored JIT bytes due to the setting of -Xscmaxjitdata is 0. |
请注意,
在清单8中,第一部分展示了缓存的填充,第二部分展示了读取缓存的类:
清单8.使用verboseIO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,verboseIO Hello [-Xshareclasses verbose I/O output enabled] Failed to find class java/lang/Object in shared cache for class-loader id 0. Stored class java/lang/Object in shared cache for class-loader id 0 with URL C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib t.jar (index 2). Failed to find class java/lang/J9VMInternals in shared cache for class-loader id 0. Stored class java/lang/J9VMInternals in shared cache for class-loader id 0 with URL C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib t.jar (index 2). Failed to find class com/ibm/oti/vm/VM in shared cache for class-loader id 0. Stored class com/ibm/oti/vm/VM in shared cache for class-loader id 0 with URL C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib t.jar (index 2). Failed to find class java/lang/J9VMInternals$ClassInitializationLock in shared cache for class-loader id 0. … … C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,verboseIO Hello [-Xshareclasses verbose I/O output enabled] Found class java/lang/Object in shared cache for class-loader id 0. Found class java/lang/J9VMInternals in shared cache for class-loader id 0. Found class com/ibm/oti/vm/VM in shared cache for class-loader id 0. Found class java/lang/J9VMInternals$ClassInitializationLock in shared cache for class-loader id 0. … … |
清单9中所示的
清单9. Helper API的状态输出
1 2 3 4 5 6 7 8 9 10 11 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,verboseHelper Hello [-Xshareclasses Helper API verbose output enabled] Info for SharedClassURLClasspathHelper id 1: verbose output enabled for SharedClassURLClasspathHelper id 1 Info for SharedClassURLClasspathHelper id 1: Created SharedClassURLClasspathHelper with id 1 Info for SharedClassURLClasspathHelper id 2: verbose output enabled for SharedClassURLClasspathHelper id 2 Info for SharedClassURLClasspathHelper id 2: Created SharedClassURLClasspathHelper with id 2 Info for SharedClassURLClasspathHelper id 1: There are no confirmed elements in the classpath. Returning null. Info for SharedClassURLClasspathHelper id 2: There are no confirmed elements in the classpath. Returning null. Info for SharedClassURLClasspathHelper id 2: setClasspath() updated classpath. No invalid URLs found Info for SharedClassURLClasspathHelper id 2: Number of confirmed entries is now 1 Hello |
清单10中所示的
清单10.有关AOT加载和存储的详细信息
1 2 3 4 5 6 7 8 | C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=demo,verboseAOT -Xjit:verbose -cp shcdemo.jar ClassLoadStress … + (AOT cold) java/nio/Bits.makeChar(BB)C @ 0x00000000540049E0-0x0000000054004ABF OrdinaryMethod - Q_SZ=2 Q_SZI=2 QW=6 j9m=0000000004A4B690 bcsz=12 GCR compThread=1 CpuLoad=298%(37%avg) JvmCpu=175% Stored AOT code for ROMMethod 0x00000000123C2168 in shared cache. … + (AOT load) java/lang/String.substring(II)Ljava/lang/String; @ 0x0000000054017728-0x00000000540179DD Q_SZ=0 Q_SZI=0 QW=1 j9m=00000000049D9DF0 bcsz=100 compThread=0 Found AOT code for ROMMethod 0x0000000012375700 in shared cache. … |
第1部分仅此而已,请务必在明天讨论Eclipse OpenJ9中的类共享的下一步时进行调整。