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

Class Sharing in Eclipse OpenJ9: How to Improve Memory, Performance (Part 2)

内存占用量和启动时间是Java虚拟机(JVM)的重要性能指标。 在云环境中,内存占用变得尤为重要,因为您需要为应用程序使用的内存付费。 在本教程中,我们将向您展示如何使用Eclipse OpenJ9中的共享类功能来减少内存占用并改善JVM启动时间。

运行时字节码修改

运行时字节码修改是一种将行为检测到Java类中的流行方法。 可以使用JVM工具接口(JVMTI)挂钩执行此操作(请在此处找到详细信息)。 或者,可以在定义类之前用类加载器替换类字节。 这给类共享带来了额外的挑战,因为一个JVM可能会缓存检测到的字节码,而该字节码不应由共享同一缓存的另一个JVM加载。

但是,由于OpenJ9 Shared Classes实现的动态性质,使用不同修改类型的多个JVM可以安全地共享同一缓存。 确实,如果字节码修改很昂贵,则缓存修改后的类会带来更大的好处,因为该转换只需执行一次即可。 唯一的规定是字节码修改应是确定性的和可预测的。 一旦修改并缓存了一个类,就无法再对其进行更改。

可以使用-Xshareclassesmodified=子选项来共享修改的字节码。 上下文是用户定义的名称,该名称在共享缓存中创建逻辑分区,该JVM加载的所有类都存储在该逻辑分区中。 使用该特定修改的所有JVM应该使用相同的修改上下文名称。 它们都从同一个共享缓存分区中加载类。 使用不带modified ub-option的相同共享缓存的任何JVM都会正常查找并存储普通类。

潜在的陷阱

如果JVM与已注册用于修改类字节的JVMTI代理一起运行,并且未使用已修改的子选项,则与其他原始JVM或使用其他代理的JVM进行类共享仍将得到安全管理,尽管这样做会降低性能成本。 额外检查。 因此,使用修改后的子选项总是更有效。

请注意,这仅是可能的,因为JVM由于使用JVMTI API而知道何时进行字节码修改。 重新定义和重新转换的类不存储在缓存中。 JVM将原始类字节数据存储在共享高速缓存中,这允许为从高速缓存加载的所有类触发JVMTI ClassFileLoadHook事件。 因此,如果自定义类装入器在定义类之前先修改类字节,而不使用JVMTI和修改后的子选项,则假定所定义的类是原始类,并且可能被其他JVM错误地装入。

有关共享修改后的字节码的更多详细信息,请参见此处。

使用助手API

OpenJ9提供了Shared Classes Helper API,以便开发人员可以将类共享支持集成到自定义类加载器中。 这仅对于不扩展java.net.URLClassLoader的类加载器是必需的,因为那些类加载器会自动继承类共享支持。

关于Helper API的全面教程超出了本文的范围,但是我们将提供一个概述。 如果您想了解更多详细信息,可以在GitHub上找到Helper API实现。

助手API:摘要

所有Helper API类都在com.ibm.oti.shared包中。 每个希望共享类的类加载器必须从SharedClassHelperFactory获取一个SharedClassHelper对象。 创建后,SharedClassHelper属于请求它的类加载器,并且只能存储该类加载器定义的类。 SharedClassHelper为类加载器提供了一个简单的API,用于在共享缓存中查找和存储类。 如果类加载器是垃圾收集的,则其SharedClassHelper也是垃圾收集的。

使用SharedClassHelperFactory

SharedClassHelperFactory是使用静态方法com.ibm.oti.shared.Shared.getSharedClassHelperFactory()获得的单例,如果在JVM中启用了类共享,则该方法将返回工厂。 否则,返回null。

使用SharedClassHelpers

工厂可以返回三种不同的SharedClassHelper类型。 每个设计用于不同类型的类加载器:

  • SharedClassURLClasspathHelper:此帮助程序设计用于具有URL类路径概念的类加载器。 使用URL类路径数组在共享缓存中存储并找到类。 必须在文件系统上访问类路径中的URL资源,才能缓存这些类。 该帮助程序还对如何在其生存期内修改类路径进行了一些限制。

  • SharedClassURLHelper:此帮助程序设计用于可以从任何URL加载类的类加载器。 给定的URL资源必须在文件系统上可访问,才能缓存这些类。

  • SharedClassTokenHelper:此帮助程序有效地将共享类高速缓存转换为简单的哈希表-使用针对共享高速缓存无意义的字符串键令牌存储类。 这是唯一不提供动态更新功能的帮助程序,因为存储的类没有与之关联的文件系统上下文。

  • 每个SharedClassHelper都有两种基本方法,辅助函数类型之间的参数略有不同:

  • 在类加载器向其父类请求类(如果存在)之后,应调用byte[] findSharedClass(String classname...)。 如果findSharedClass()不返回null,则类加载器应在返回的字节数组上调用defineClass()。 请注意,此函数为defineClass()返回一个特殊的cookie,而不是实际的类字节,因此无法检测这些字节。

  • 定义类后,应立即调用boolean storeSharedClass(Class clazz...)。 如果该类已成功存储,则该方法返回true,否则返回false。

  • 其他注意事项

    在与应用程序部署类共享时,您需要考虑诸如安全性和缓存调整之类的因素。 这些注意事项简要总结如下。

    安全性

    默认情况下,共享缓存是使用用户级安全性创建的,因此只有创建共享缓存的用户才能访问它。 因此,每个用户的默认缓存名称都不同,从而避免了冲突。 在UNIX上,有一个子选项可指定groupAccess,该选项可访问创建缓存的用户的主要组中的所有用户。

    除此之外,如果安装了SecurityManager,则只有明确授予了正确的权限,类加载器才能共享类。 有关设置这些权限的更多详细信息,请参阅此处的用户指南。

    垃圾收集和即时编译

    在启用类共享的情况下运行对类垃圾回收(GC)无效。 就像在非共享情况下一样,类和类加载器仍会被垃圾回收。 此外,使用类共享时,对GC模式或配置也没有任何限制。

    无法将即时(JIT)编译的代码缓存在类缓存中。 共享缓存中的AOT代码也需要进行JIT编译,并且会影响方法的方式和时间。 此外,JIT提示和配置文件数据可以存储在共享缓存中。 您可以使用选项Xscmaxjitdata-Xscminjitdata设置此类JIT数据的共享缓存空间的大小。

    缓存大小限制

    当前最大理论高速缓存大小为2GB。 缓存大小受诸如可用系统内存,可用虚拟地址空间,可用磁盘空间等因素的限制。有关更多详细信息,请参见此处。

    示例

    为了实际演示类共享的好处,本节提供了一个简单的图形演示。 源代码和二进制文件可在GitHub上获得。

    该演示应用程序可在Java 8上运行,并查找jre\lib目录并打开每个JAR,并在找到的每个类上调用Class.forName()。 这导致将大约16,000个类加载到JVM中。 该演示报告了JVM加载类所需的时间。 这是一个人为设计的示例,但是有效地展示了类共享的好处。 让我们运行该应用程序并查看结果。

    类加载效果

    1.从Adopt OpenJDK项目中下载带有OpenJ9的JDK或从docker映像中提取。

    2.从GitHub下载shcdemo.jar。

    3.使用清单11中的命令,运行几次没有类共享的测试来预热系统磁盘缓存:

    清单11.预热磁盘缓存

    1
    C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:none -cp shcdemo.jar ClassLoadStress

    当出现图1中的窗口时,按按钮。 该应用程序将加载类。

    图1.按下按钮

    Figure 1

    一旦加载了类,应用程序将报告加载了多少以及花费了多长时间,如图2所示:

    图2.结果在!

    Figure 2

    您会注意到,每次运行该应用程序时,它的运行速度可能都会有所提高。 这是因为操作系统优化。

    4.现在,运行启用了类共享的演示,如清单12所示。 创建一个新的共享缓存。 您可以指定大约50MB的缓存大小,以确保所有类都有足够的空间。 清单12显示了命令行和一些示例输出。

    清单12.在启用类共享的情况下运行演示

    1
    2
    3
    4
    5
    6
    7
    8
    C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -cp shcdemo.jar -Xshareclasses:name=demo,verbose -Xscmx50m ClassLoadStress
    [-Xshareclasses persistent cache enabled]
    [-Xshareclasses verbose output enabled]
    JVMSHRC236I Created shared classes persistent cache demo
    JVMSHRC246I Attached shared classes persistent cache demo
    JVMSHRC765I Memory page protection on runtime data, string read-write data and partially filled pages is successfully enabled
    JVMSHRC168I Total shared class bytes read=1111375. Total bytes stored=40947096
    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.

    您还可以使用printStats检查缓存统计信息,如清单13所示:

    清单13.检查缓存类的数量

    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 -cp shcdemo.jar -Xshareclasses:name=demo,printStats

    Current statistics for cache"demo":

    Cache created with:
            -Xnolinenumbers                      = false
            BCI Enabled                          = true
            Restrict Classpaths                  = false
            Feature                              = cr

    Cache contains only classes with line numbers

    base address                         = 0x0000000011F96000
    end address                          = 0x0000000015140000
    allocation pointer                   = 0x000000001403FF50

    cache size                           = 52428192
    softmx bytes                         = 52428192
    free bytes                           = 10874992
    ROMClass bytes                       = 34250576
    AOT bytes                            = 1193452
    Reserved space for AOT bytes         = -1
    Maximum space for AOT bytes          = -1
    JIT data bytes                       = 28208
    Reserved space for JIT data bytes    = -1
    Maximum space for JIT data bytes     = -1
    Zip cache bytes                      = 902472
    Data bytes                           = 351648
    Metadata bytes                       = 661212
    Metadata % used                      = 1%
    Class debug area size                = 4165632
    Class debug area used bytes          = 3911176
    Class debug area % used              = 93%

    # ROMClasses                         = 17062
    # AOT Methods                        = 559
    # Classpaths                         = 3
    # URLs                               = 0
    # Tokens                             = 0
    # Zip caches                         = 5
    # Stale classes                      = 0
    % Stale classes                      = 0%

    Cache is 79% full

    Cache is accessible to current user = true

    5.现在,使用相同的Java命令行再次启动演示。 这次,它应该从共享类缓存中读取类,如清单14所示。

    清单14.使用热共享缓存运行应用程序

    1
    2
    3
    4
    5
    6
    7
    8
    C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -cp shcdemo.jar -Xshareclasses:name=demo,verbose -Xscmx50m ClassLoadStress
    [-Xshareclasses persistent cache enabled]
    [-Xshareclasses verbose output enabled]
    JVMSHRC237I Opened shared classes persistent cache demo
    JVMSHRC246I Attached shared classes persistent cache demo
    JVMSHRC765I Memory page protection on runtime data, string read-write data and partially filled pages is successfully enabled
    JVMSHRC168I Total shared class bytes read=36841382. Total bytes stored=50652
    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.

    从图3可以清楚地看到类加载时间的显着改善(大约40%)。由于操作系统的优化,每次运行演示时,性能应该都会略有提高。

    图3.热缓存结果

    Image title

    您可以尝试一些变体。 例如,您可以使用javaw命令启动多个演示并一起触发所有加载类以查看并发性能。

    在实际情况下,使用类共享可以获得的总体JVM启动时间收益取决于应用程序加载的类数。 HelloWorld程序不会带来太大好处,而大型Web服务器当然可以。 但是,该示例有望证明,尝试类共享非常简单,因此您可以轻松地测试其好处。

    内存占用

    在多个JVM中运行示例程序时,也很容易看到节省的内存。

    下面是使用与前面的示例相同的计算机获得的四个VMMap快照。 在图4中,该演示的两个实例已经运行完毕而没有类共享。 在图5中,使用与以前相同的命令行,在启用类共享的情况下运行了两个实例以完成操作。

    图4.两个没有类共享的演示实例

    Figure 4-1

    Image title

    图5.启用了类共享的两个演示实例

    Image title

    Figure 5-2

    在实验中,共享缓存的大小为50MB,因此与图5相比,图6中每个实例的"映射文件"大小增加了50MB(56736KB – 5536KB)。

    您可以清楚地看到启用共享类时的内存使用量(私有WS)明显较低。 对于2个JVM实例,可以节省大约70MB的私有WS。 如果在启用了类共享的情况下启动了更多的演示实例,则会节省更多的内存。 上面的测试结果是在具有32GB RAM的Windows 10笔记本电脑上,使用Intel?Core?i7-6820HQ CPU @ 2.70GHz获得的。

    我们也在Linux x64机器上执行相同的内存占用量实验。 清单15显示了两个没有类共享的JVM实例的结果,清单16显示了两个启用了类共享的JVM实例的结果。

    从结果来看,启用类共享时,RSS并没有显示太多改进。 这是因为整个共享缓存都包含在RSS中。 但是,如果我们看一下PSS,该PSS仅占每个JVM共享缓存的一半(因为它由2个JVM共享),则节省了大约34MB。

    清单15.禁用类共享的Linux上的足迹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pmap -X 9612
    9612:   xa6480_openj9/j2sdk-image/jre/bin/java -cp shcdemo.jar ClassLoadStress
    Address Perm  …   Size    Rss     Pss Referenced Anonymous Swap Locked Mapping

                    ======= ======= ===== ========   ========= ==== ====
                    2676500 118280 106192 118280     95860     0    0 KB
    pmap -X 9850
    9850:   xa6480_openj9/j2sdk-image/jre/bin/java -cp shcdemo.jar ClassLoadStress
    Address Perm  …   Size    Rss     Pss Referenced Anonymous Swap Locked Mapping

                    ======= ======= ===== ========   ========= ==== ====
                    2676500 124852 112792 124852     102448    0    0 KB

    清单16.启用类共享的Linux上的足迹

    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
    pmap -X 4501
    4501:   xa6480_openj9/j2sdk-image/jre/bin/java -Xshareclasses:name=demo -Xscmx50m -cp shcdemo.jar ClassLoadStress
    Address Perm  …   Size    Rss     Pss Referenced Anonymous Swap Locked Mapping

    7fe7d0e00000 rw-s 4       4       2       4        0    0      0 C290M4F1A64P_demo_G35
    7fe7d0e01000 r--s 33356   33356   16678   33356    0    0      0 C290M4F1A64P_demo_G35
    7fe7d2e94000 rw-s 11096   48      24      48       0    0      0 C290M4F1A64P_demo_G35
    7fe7d396a000 r--s 5376    1640    832     1640     0    0      0 C290M4F1A64P_demo_G35
    7fe7d3eaa000 rw-s 296     0       0       0        0    0      0 C290M4F1A64P_demo_G35
    7fe7d3ef4000 r--s 1072    0       0       0        0    0      0 C290M4F1A64P_demo_G35

                      ======= ======= ===== ======== ====== ====== ====
                      2732852 120656  90817 97988    62572  0      0 KB
    pmap -X 4574
    4574:   xa6480_openj9/j2sdk-image/jre/bin/java -Xshareclasses:name=demo -Xscmx50m -cp shcdemo.jar ClassLoadStress
    Address Perm  …   Size    Rss     Pss Referenced Anonymous Swap Locked Mapping

    7f308ce00000 rw-s 4       4       2       4        0    0      0 C290M4F1A64P_demo_G35
    7f308ce01000 r--s 33356   33356   16678   33356    0    0      0 C290M4F1A64P_demo_G35
    7f308ee94000 rw-s 11080   48      24      48       0    0      0 C290M4F1A64P_demo_G35
    7f308f966000 r--s 5392    1632    824     1632     0    0      0 C290M4F1A64P_demo_G35
    7f308feaa000 rw-s 296     0       0       0        0    0      0 C290M4F1A64P_demo_G35
    7f308fef4000 r--s 1072    0       0       0        0    0      0 C290M4F1A64P_demo_G35

                      ======= ======= ===== ======== ====== ====== ====
                      2730800 122832  92911 102584   64812  0      0 KB

    结论

    OpenJ9实现中的"共享类"功能提供了一种简单而灵活的方式来减少内存占用并缩短JVM启动时间。 在本文中,您已经了解了如何启用该功能,如何使用缓存实用程序以及如何对收益进行量化。