Eclipse OpenJ9中的类共享:如何提高内存和性能(第1部分)

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映像中将其拉出(如果您想按照示例进行操作)。

工作原理

让我们从探讨共享类功能的运行方式的技术细节开始。

启用课程共享

要启用类共享,请将-Xshareclasses[:name=]添加到现有的Java命令行中。 JVM启动时,它将查找具有给定名称的共享缓存(如果未提供名称,它将使用当前用户名)。 它要么连接到现有的共享缓存,要么创建一个新的。

您可以使用参数-Xscmx[k|m|g]指定共享缓存的大小。 仅当创建新的共享缓存时,此参数才适用。 如果省略此选项,则使用平台相关的默认值。 请注意,有些操作系统设置会限制您可以分配的共享内存量。 例如,Linux上的SHMMAX通常设置为大约32MB。 要了解有关这些设置的详细信息,请参阅本用户指南的"共享类"部分。

共享类缓存

共享类高速缓存由固定大小的共享内存组成,该内存在JVM的生命周期或系统重新启动后将一直存在,除非使用了非持久性共享高速缓存。 系统上可以存在任何数量的共享缓存,并且所有缓存均受操作系统设置和限制的约束。

没有JVM拥有共享缓存,也没有主/从JVM概念。 相反,任意数量的JVM可以同时读取和写入共享缓存。

共享缓存的大小不能增加。 当它变满时,JVM仍然可以从中加载类,但是它不再可以在其中存储任何数据。 您可以先创建一个大型的共享类高速缓存,同时为可以使用多少共享高速缓存空间设置一个软的最大限制。 当您想将更多数据存储到共享缓存中而不关闭与之连接的JVM时,可以增加此限制。 请查看OpenJ9文档,以获取有关软最大限制的更多详细信息。

此外,还有几种JVM实用程序可管理主动共享的缓存。 我们将在下面的"共享类实用程序"部分中讨论这些内容。

使用JVM命令行显式销毁共享缓存时,会将其删除。

如何缓存类?

当JVM加载一个类时,它首先在类加载器高速缓存中查找以查看其所需的类是否已经存在。 如果是,它将从类加载器缓存中返回该类。 否则,它将从文件系统加载类,并将其作为defineClass()调用的一部分写入缓存。 因此,非共享的JVM具有以下类加载器查找顺序:

  • 类加载器缓存

  • 父母

  • 文件系统

  • 相反,运行具有类共享功能的JVM使用以下顺序:

  • 类加载器缓存

  • 父母

  • 共享类缓存

  • 文件系统

  • 使用公共Helper API从共享类缓存中读取和写入类。 Helper API已集成到java.net.URLClassLoader(在Java 9及更高版本中为jdk.internal.loader.BuiltinClassLoader)。 因此,任何扩展java.net.URLClassLoader的类加载器均免费获得类共享支持。 对于自定义类加载器,OpenJ9提供了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编译的代码时性能下降,从而缩短了启动时间。 创建新的共享缓存时,可以使用选项-Xscminaot-Xscmaxaot 设置共享缓存中AOT空间的大小。 如果没有同时使用-Xscminaot和-Xscmaxaot,则只要有可用空间,AOT代码就会存储到共享缓存中。

    如果文件系统上的类发生更改会发生什么?

    由于共享类高速缓存可以无限期地保留,因此可能会发生文件系统更新,这些更新会使共享高速缓存中的类和AOT代码无效。 如果类加载器请求共享类,则返回的类应始终与从文件系统加载的类相同。 这在加载类时透明地发生,因此,在知道始终加载正确的类的情况下,用户可以在共享类缓存的生存期内修改和更新任意数量的类。

    类更改的陷阱:示例

    想象一下由JVM存储到共享缓存中的类C1。 然后,当JVM关闭时,将更改C1并重新编译。 JVM重新启动时,不应加载C1的缓存版本。

    类似地,想象一下以/mystuff:/mystuff/myClasses.jar的类路径运行的JVM。 它将C2从myClasses.jar加载到共享缓存中。 然后,将另一个C2.class添加到 /myStuff,并且另一个JVM启动运行相同的应用程序。 对于JVM来说,加载缓存的C2版本是不正确的。

    JVM通过将时间戳记值存储到共享缓存中并在每次类加载时将缓存的值与实际值进行比较来检测文件系统更新。 如果检测到JAR文件已更新,则不知道已更改了哪些类。 因此,所有类以及来自缓存中该JAR的AOT代码都会立即标记为过期,并且无法从缓存中加载。 当从文件系统中加载该JAR中的类并将其重新添加到缓存时,仅完整地添加已更改的类;否则,将仅添加已更改的类。 那些没有改变的东西实际上已经过时了。

    无法从共享类高速缓存中清除类,但是JVM会尝试最有效地利用其拥有的空间。 例如,即使从许多不同的位置加载同一类,也永远不会添加两次。 因此,如果通过三个不同的JVM从/A.jar/B.jar/C.jar加载相同的类C3,则该类数据仅添加一次。 但是,有三部分元数据来描述从中加载它的三个位置。

    共享类实用程序

    您可以使用多种实用程序来管理共享类缓存,所有实用程序都是-Xshareclasses 的子选项(您可以通过java -Xshareclasses:help获取所有子选项的完整列表)。

    为了演示这些选项的用法,让我们来看一些示例。

    首先,让我们通过运行具有不同缓存名称的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

    运行listAllCaches子选项会列出系统上的所有缓存,并确定它们是否正在使用,如清单2所示:

    列出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

    运行printStats选项将在已命名的缓存上打印摘要统计信息,如清单3所示。 有关printStats选项的详细说明,请参见用户指南。

    清单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

    还有其他printStats子选项可用于打印共享缓存中的特定数据。 它们可以在printStats=help中找到。 例如,您可以通过printStats=classpath检查类路径数据:

    清单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

    共享缓存使用destroy选项销毁,如清单5所示。类似地,选项destroyAll销毁所有未使用且用户有权销毁的共享缓存。

    清单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中所示的expire选项是一个管理选项,您可以将其添加到命令行以自动破坏在指定的分钟数内未附加任何内容的缓存。 清单6查找一周(10080分钟)内未使用的缓存,并在启动JVM之前将其销毁。

    reset选项始终创建一个新的共享缓存。 如果存在具有相同名称的缓存,则将其销毁并创建一个新的缓存。

    清单6.销毁一周内未使用的缓存

    1
    2
    C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,expire=10080 Hello
    Hello

    详细选项

    详细选项提供有关类共享正在做什么的有用反馈。 它们都是-Xshareclasses的子选项。 本节提供了一些有关如何使用这些详细选项的示例。

    清单7中所示的verbose选项给出了有关JVM启动和关闭的简要状态信息:

    清单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.

    verboseIO选项将每个类加载请求的状态行打印到共享缓存。 要了解verboseIO的输出,您应该了解类加载器的层次结构。 对于由任何非引导类加载器加载的类,可以清楚地看到这一点。 在输出中,为每个类加载器分配了唯一的ID,但是引导加载器始终为0。

    请注意,verboseIO有时会显示从磁盘加载并存储在缓存中的类,即使它们已经被缓存也是正常的。 例如,从应用程序类路径上的每个JAR加载的第一类始终从磁盘加载并存储,而不管其是否存在于缓存中。 <!-[如果!supportAnnotations]->这是为了确认文件系统中确实存在类路径中的JAR。

    在清单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中所示的verboseHelper子选项是一个高级选项,它提供了Helper API的状态输出。 verboseHelper子选项可帮助开发人员使用Helper API来了解其驱动方式。 JVM诊断指南中描述了有关此输出的更多详细信息。

    清单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中所示的verboseAOT -Xjit:verbose子选项为您提供有关AOT从共享缓存中加载和存储活动的信息。

    清单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中的类共享的下一步时进行调整。