Contents


Class sharing in Eclipse OpenJ9

Reduce your memory footprint and improve startup performance with the shared class feature

Comments

Memory footprint and startup time are important performance metrics for a Java virtual machine (JVM). The memory footprint becomes especially important in the cloud environment since you pay for the memory your application uses. In this tutorial, we will show you how to use the shared classes feature in Eclipse OpenJ9 to reduce the memory footprint and improve your JVM startup time.

In the OpenJ9 implementation, all systems, application classes and ahead-of-time (AOT) compiled code can be stored in a dynamic class cache in shared memory. This shared classes feature is implemented on all the platforms that OpenJ9 supports. The feature even supports integration with runtime bytecode modification, which this article discusses later.

The shared classes feature is one that you don’t have to think about once it’s started, but it provides a powerful scope for reducing memory footprint and improving JVM startup time. For this reason, it is best suited to environments where more than one JVM is running similar code or where a JVM is regularly restarted.

In addition to the runtime class-sharing support in the JVM and its class loaders, there is also a public Helper API provided for integrating class sharing support into custom class loaders, which is discussed later in detail.

You can download the JDK with OpenJ9 from the Adopt OpenJDK project or pull from the docker image, if you'd like to follow along when we get to the example.

How it works

Let's start by exploring the technical details of how the shared classes feature operates.

Enabling class sharing

To enable class sharing, add -Xshareclasses[:name=<cachename>] to an existing Java command line. When the JVM starts up, it looks for a shared cache of the name given (if no name is provided, it uses the current user name) and it either connects to an existing shared cache or creates a new one, as required.

You can specify the shared cache size using the parameter -Xscmx<size>[k|m|g. This parameter only applies if a new shared cache is created. If this option is omitted, a platform-dependent default value is used. Note that there are operating system settings that limit the amount of shared memory you can allocate. For instance, SHMMAX on Linux is typically set to about 32MB. To learn more about the details of these settings, see the the Shared Classes section of the relevant user guide.

The shared classes cache

A shared classes cache is an area of shared memory of a fixed size that persists beyond the lifetime of the JVM or a system reboot unless a non-persistent shared cache is used. Any number of shared caches can exist on a system, and all are subject to operating system settings and restrictions.

No JVM owns the shared cache, and there is no master/slave JVM concept. Instead, any number of JVMs can read and write to the shared cache concurrently.

A shared cache cannot grow in size. When it becomes full, JVMs can still load classes from it but can no longer store any data into it. You can create a large, shared classes cache up front, while setting a soft maximum limit on how much shared cache space can be used. You can increase this limit when you want to store more data into the shared cache without shutting down the JVMs that are connected to it. Check out the OpenJ9 documentation for more details about the soft maximum limit.

In addition, there are several JVM utilities to manage active caches. We will discuss these in the shared classes utilities section below.

A shared cache is deleted when it is explicitly destroyed using a JVM command line.

How are classes cached?

When a JVM loads a class, it first looks in the class loader cache to see if the class it needs is already present. If yes, it returns the class from the class loader cache. Otherwise, it loads the class from the filesystem and writes it into the cache as part of the defineClass() call. Therefore, a non-shared JVM has the following class loader lookup order:

  1. Classloader cache
  2. Parent
  3. Filesystem

In contrast, a JVM running with the class sharing feature uses the following order:

  1. Classloader cache
  2. Parent
  3. Shared classes cache
  4. Filesystem

Classes are read from and written to the shared classes cache using the public Helper API. The Helper API is integrated into java.net.URLClassLoader (and jdk.internal.loader.BuiltinClassLoader in Java 9 and up). Therefore, any class loader that extends java.net.URLClassLoader gets class sharing support for free. For custom class loaders, OpenJ9 has provided HelperAPIs so that class sharing can be implemented on custom class loaders.

What is cached?

A shared classes cache can contain bootstrap and application classes, metadata that describes the classes, and ahead-of-time (AOT) compiled code.

Inside the OpenJ9 implementation, Java classes are divided into two parts:

  • a read-only part called a ROMClass, which contains all of the class's immutable data
  • a RAMClass that contains mutable data, such as static class variables

A RAMClass points to data in its ROMClass, but these two are completely separated. So, it is quite safe for a ROMClass to be shared between JVMs and also between RAMClasses in the same JVM.

In the non-shared case, when the JVM loads a class, it creates the ROMClass and the RAMClass separately and stores them both in its local process memory. In the shared case, if the JVM finds a ROMClass in the shared classes cache, it only needs to create the RAMClass in its local memory; the RAMClass then references the shared ROMClass.

Because most of the class data is stored in the ROMClass, this is where the memory savings are made (see a more detailed discussion in the Memory footprint sections). JVM startup times are also significantly improved with a populated cache because some of the work to define each cached class has already been done and the classes are loaded from memory, rather than from the filesystem. Startup time overhead to populate a new shared cache is not significant, as each class simply needs to be relocated into the shared cache as it is defined.

AOT compiled code is also stored into the shared cache. When the shared classes cache is enabled, the AOT compiler is automatically activated. AOT compilation allows the compilation of Java classes into native code for subsequent executions of the same program. The AOT compiler generates native code dynamically while an application runs and caches any generated AOT code in the shared classes cache. Usually the execution of AOT compiled code is faster than interpreted byte code, but not as fast as JIT’ed code. Subsequent JVMs that execute the method can load and use the AOT code from the shared cache without incurring the performance decrease experienced with generating JIT-compiled code, resulting in a faster startup time. When creating a new shared cache, you can use options -Xscminaot<x> and -Xscmaxaot<x> to set the size of AOT space in the shared cache. If neither -Xscminaot nor -Xscmaxaot is used, the AOT code will be stored to the shared cache as long as there is free space available.

What happens if a class changes on the filesystem?

Because the share classes cache can persist indefinitely, filesystem updates that invalidate classes and AOT code in the shared cache may occur. If a class loader makes a request for a shared class, then the class returned should always be the same as the one that would have been loaded from the filesystem. This happens transparently when classes are loaded, so users can modify and update as many classes as they like during the lifetime of a shared classes cache, knowing that the correct classes are always loaded.

Pitfalls with class changes: Two examples

Imagine a class C1 that is stored into the shared cache by a JVM. Then, when the JVM shuts down, C1 is changed and recompiled. When the JVM restarts, it should not load the cached version of C1.

Similarly, imagine a JVM that's running with a class path of /mystuff:/mystuff/myClasses.jar. It loads C2 from myClasses.jar into the shared cache. Then a different C2.class is added to /myStuff and another JVM starts up running the same application. It would be incorrect for the JVM to load the cached version of C2.

The JVM detects filesystem updates by storing timestamp values into the shared cache and comparing the cached values with actual values on each class load. If it detects that a JAR file has been updated, it has no idea which classes have been changed, so all classes as well as AOT code from that JAR in the cache are immediately marked as stale and cannot then be loaded from the cache. When the classes from that JAR are loaded from the filesystem and re-added to the cache, only the ones that have changed are added in their entirety; those that haven't changed are effectively made not stale.

Classes cannot be purged from the shared classes cache, but the JVM attempts to make the most efficient use of the space it has. For example, the same class is never added twice, even if it is loaded from many different locations. So, if the same class C3 is loaded from /A.jar, /B.jar, and /C.jar by three different JVMs, the class data is only added once, but there are three pieces of metadata to describe the three locations from which it was loaded.

Shared classes utilities

There are several utilities that you can use to manage shared classes caches, all of which are sub-options to -Xshareclasses. (You can get a complete list of all sub-options via java -Xshareclasses:help.)

To demonstrate the use of these options, let's walk through some examples.

First, let's create two shared caches by running a Hello class with different cache names, as Listing 1 shows:

Listing 1. Creating two shared caches
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

Running the listAllCaches sub-option lists all caches on a system and determines whether they are in use, as you can see in Listing 2:

Listing 2. Listing all shared caches
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

Running the printStats option prints summary statistics on the named cache, as Listing 3 shows. For a detailed description of the printStats option, see the user guide.

Listing 3. Summary statistics for a shared cache
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

There are other printStats sub-options that can be used to print specific data in the shared cache. They can be found in printStats=help. For example, you can check the class path data via printStats=classpath:

Listing 4. Listing the class path contents of a shared cache
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\rt.jar
        C:\OpenJ9\wa6480_openj9\j2sdk-image\jre\lib\resources.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
…
…

The shared caches are destroyed using the destroy option, illustrated in Listing 5. Similarly, destroyAll destroys all caches that are not in use and that the user has permissions to destroy.

Listing 5. Destroying a cache
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.

The expire option, illustrated in Listing 6, is a housekeeping option that you can add to the command line to automatically destroy caches to which nothing has been attached for a specified number of minutes. Listing 6 looks for caches that have not been used for a week (10,080 minutes) and destroys them before starting the JVM.

The reset option always creates a new shared cache. If a cache with the same name exists, it is destroyed and a new one is created.

Listing 6. Destroying caches that haven't been used in a week
C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:name=Cache1,expire=10080 Hello
Hello

Verbose options

Verbose options provide useful feedback on what class sharing is doing. They are all sub-options to -Xshareclasses. This section offers some examples of how to use verbose options.

The verbose option, illustrated in Listing 7, gives concise status information on JVM startup and shutdown:

Listing 7. Getting JVM status information
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.

The verboseIO option prints a status line for every class load request to the shared cache. To understand verboseIO output, you should understand the class loader hierarchy, as this can be clearly seen for classes that are loaded by any non-bootstrap class loader. In the output, each class loader is assigned a unique ID, but the bootstrap loader is always 0.

Note that it is normal for verboseIO to sometimes show classes being loaded from disk and stored in the cache even if they are already cached. For example, the first class loaded from each JAR on the application class path is always loaded from disk and stored, regardless of whether it exists in the cache or not. This is to confirm the JAR in the class path does exist on the file system.

In listing 8, the first section demonstrates the population of the cache and the second section shows the reading of the cached classes:

Listing 8. Using verboseIO
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\rt.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\rt.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\rt.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.
…
…

The verboseHelper sub-option, illustrated in listing 9, is an advanced option that gives status output from the Helper API. The verboseHelper sub-option helps developers using the Helper API understand how it is being driven. More details on this output are described in the JVM diagnostics guide.

Listing 9. Status output from the Helper API
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

The verboseAOT and -Xjit:verbose sub-option, illustrated in listing 10, give you information on AOT loading and storing activities from/into the shared cache.

Listing 10. Verbose information on AOT loading and storing
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.
…

Runtime bytecode modification

Runtime bytecode modification is a popular way of instrumenting behaviour into Java classes. It can be performed using the JVM Tools Interface (JVMTI) hooks (details can be found here). Alternately, the class bytes can be replaced by the class loader before the class is defined. This presents an extra challenge to class sharing, as one JVM may cache instrumented bytecode that should not be loaded by another JVM sharing the same cache.

However, because of the dynamic nature of the OpenJ9 Shared Classes implementation, multiple JVMs using different types of modification can safely share the same cache. Indeed, if the bytecode modification is expensive, caching the modified classes has an even greater benefit, as the transformation only needs to be performed once. The only provision is that the bytecode modifications should be deterministic and predictable. Once a class has been modified and cached, it cannot then be changed further.

Modified bytecode can be shared by using the modified=<context> sub-option to -Xshareclasses. The context is a user-defined name that creates a logical partition in the shared cache into which all the classes loaded by that JVM are stored. All JVMs using that particular modification should use the same modification context name, and they all load classes from the same shared cache partition. Any JVM using the same shared cache without the modified sub-option finds and stores vanilla classes as normal.

Potential pitfalls

If a JVM is running with a JVMTI agent that has registered to modify class bytes and the modified sub-option is not used, class sharing with other vanilla JVMs or with JVMs using other agents is still managed safely, albeit with a small performance cost because of extra checking. Thus, it is always more efficient to use the modified sub-option.

Note that this is only possible because the JVM knows when bytecode modification is occurring because of the use of the JVMTI API. Redefined and retransformed classes are not stored in the cache. JVM stores vanilla class byte data in the shared cache, which allows the JVMTI ClassFileLoadHook event to be triggered for all classes loaded from the cache. Therefore, if a custom class loader modifies class bytes before defining the class without using JVMTI and without using the modified sub-option, the classes being defined are assumed to be vanilla and could be incorrectly loaded by other JVMs.

For more detailed information on sharing modified bytecode, see here.

Using the Helper API

The Shared Classes Helper API was provided by OpenJ9 so that developers can integrate class sharing support into custom class loaders. This is only required for class loaders that do not extend java.net.URLClassLoader, as those class loaders automatically inherit class-sharing support.

A comprehensive tutorial on the Helper API is beyond the scope of this article, but we will provide a general overview. If you'd like to know more details, you can find the Helper API implementation on GitHub.

The Helper API: A summary

All the Helper API classes are in the com.ibm.oti.shared package. Each class loader wishing to share classes must get a SharedClassHelper object from a SharedClassHelperFactory. Once created, the SharedClassHelper belongs to the class loader that requested it and can only store classes defined by that class loader. The SharedClassHelper gives the class loader a simple API for finding and storing classes in the shared cache. If the class loader is garbage collected, its SharedClassHelper is also garbage collected.

Using the SharedClassHelperFactory

The SharedClassHelperFactory is a singleton that is obtained using the static method com.ibm.oti.shared.Shared.getSharedClassHelperFactory(), which returns a factory if class sharing is enabled in the JVM; otherwise, it returns null.

Using the SharedClassHelpers

There are three different types of SharedClassHelper that can be returned by the factory. Each is designed for use by a different type of class loader:

  • SharedClassURLClasspathHelper: This helper is designed for use by class loaders that have the concept of a URL class path. Classes are stored and found in the shared cache using the URL class path array. The URL resources in the class path must be accessible on the filesystem for the classes to be cached. This helper also carries some restrictions on how the class path can be modified during the lifetime of the helper.
  • SharedClassURLHelper: This helper is designed for use by class loaders that can load classes from any URL. The URL resources given must be accessible on the filesystem for the classes to be cached.
  • SharedClassTokenHelper: This helper effectively turns the shared class cache into a simple hash table -- classes are stored against string key tokens that are meaningless to the shared cache. This is the only helper that doesn't provide dynamic update capability because the classes stored have no filesystem context associated with them.

Each SharedClassHelper has two basic methods, the parameters of which differ slightly between helper types:

  • byte[] findSharedClass(String classname...) should be called after the class loader has asked its parent for the class (if one exists). If findSharedClass() does not return null, the class loader should call defineClass() on the byte array returned. Note that this function returns a special cookie for defineClass(), not actual class bytes, so the bytes cannot be instrumented.
  • boolean storeSharedClass(Class clazz...) should be called immediately after a class has been defined. The method returns true if the class was successfully stored and false otherwise.

Other considerations

When deploying class sharing with your application, you need to consider factors such as security and cache tuning. These considerations are briefly summarised here.

Security

By default, the shared caches are created with user-level security, so only the user that created the shared cache can access it. For this reason, the default cache name is different for each user so that clashes are avoided. On UNIX, there is a sub-option to specify groupAccess, which gives access to all users in the primary group of the user that created the cache.

In addition to this, if there is a SecurityManager installed, a class loader can only share classes if it has been explicitly granted the correct permissions. Refer to the user guide here for more details on setting these permissions.

Garbage collection and just-in-time compilation

Running with class sharing enabled has no effect on class garbage collection (GC). Classes and class loaders are still garbage collected just as they are in the non-shared case. Also, there are no restrictions placed on GC modes or configurations when using class sharing.

It is not possible to cache just-in-time (JIT) compiled code in the class cache. The AOT code in the shared cache is also subject to JIT compilation, and it affects how and when a method is JITed. In addition, the JIT hints and profile data can be stored in the shared cache. You can use options -Xscmaxjitdata<x> and -Xscminjitdata<x> to set the size for shared cache space for such JIT data.

Cache size limits

The current maximum theoretical cache size is 2GB. The cache size is limited by factors such as available system memory, available virtual address space, available disk space, etc. More details can be found here.

An example

To practically demonstrate the benefits of class sharing, this section provides a simple graphical demo. The source and binaries are available on GitHub.

The demo app works on Java 8 and looks for the jre\lib directory and opens each JAR, calling Class.forName() on every class it finds. This causes about 16,000 classes to be loaded into the JVM. The demo reports on how long the JVM takes to load the classes. This is a slightly contrived example, but it effectively demonstrates the benefits of class sharing. Let's run the application and see the results.

Class-loading performance

  1. Download JDK with OpenJ9 from the Adopt OpenJDK project or pull from the docker image.
  2. Download shcdemo.jar from GitHub.
  3. Run the test a couple of times without class sharing to warm up the system disk cache, using the command in Listing 11:
    Listing 11. Warming up the disk cache
    C:\OpenJ9>wa6480_openj9\j2sdk-image\bin\java -Xshareclasses:none -cp shcdemo.jar ClassLoadStress

    When the window in Figure 1 appears, press the button. The app will load the classes.

    Figure 1. Press the button
    load classes image
    load classes image

    Once the classes have loaded, the application reports how many it loaded and how long it took, as Figure 2 shows:

    Figure 2. Results are in!

    You'll notice that the application probably gets slightly faster each time you run it; this is because of operating system optimizations.

  4. Now run the demo with class sharing enabled, as Listing 12 illustrates. A new shared cache is created. You can specify a cache size of about 50MB to ensure that there is enough space for all the classes. Listing 12 shows the command line and some sample output.
    Listing 12. Running the demo with class sharing enabled
    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.

    You can also check the cache statistics use printStats, as Listing 13 shows:

    Listing 13. Checking the number of cached classes
    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. Now, start the demo again with the same Java command line. This time, it should read the classes from the shared class cache, as you can see here:
    Listing 14. Running the application with a warm shared cache
    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.

    You can clearly see the significant (about 40%) improvement in class load time. Again, you should see the performance improve slightly each time you run the demo because of operating system optimizations.

    Figure 3. Warm cache results

There are a few variations you can experiment with. For example, you can use the javaw command to start multiple demos and trigger them all loading classes together to see the concurrent performance.

In a real-world scenario, the overall JVM startup time benefit that can be gained from using class sharing depends on the number of classes that are loaded by the application. A HelloWorld program will not show much benefit, whereas a large Web server certainly will. However, this example has hopefully demonstrated that experimenting with class sharing is very straightforward, so you can easily test the benefits.

Memory footprint

It is also easy to see the memory savings when running the example program in more than one JVM.

Below are four VMMap snapshots obtained using the same machine as the previous examples. In Figure 4, two instances of the demo have been run to completion without class sharing. In Figure 5, two instances have been run to completion with class sharing enabled, using the same command lines as before.

Figure 4. Two instances of demo with no class sharing
Figure 5. Two instances of demo with class sharing enabled

The share cache size is 50MB in the experiment, so the Mapped Files size of each instance in Figure 5 is 50MB more (56736KB – 5536KB) compared to Figure 4.

You can clearly see that the memory usage (Private WS) when shared classes are enabled is significantly lower. A saving of about 70MB Private WS is achieved for 2 JVM instances. More memory saving will be observed if more instances of the demo are launched with class sharing enabled. The test results above are obtained on a Windows 10 laptop with 32GB RAM, using an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz.

We perform the same memory footprint experiment on a Linux x64 machine as well. Listing 15 shows the result of two JVM instances with no class sharing and Listing 16 shows the result of two JVM instances with class sharing enabled.

Looking at the results, RSS does not show much improvement when class sharing is enabled. This is because the whole shared cache is included in RSS. But if we look at the PSS, which counts only half of the shared cache to each JVM (as it is shared by these 2 JVMs), there is a saving of about 34MB.

Listing 15. Footprint on Linux with class sharing disabled
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
Listing 16. Footprint on Linux with class sharing enabled
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

Conclusion

The Shared Classes feature in the OpenJ9 implementation offers a simple and flexible way to reduce memory footprint and improve JVM startup time. In this article, you have seen how to enable the feature, how to use the cache utilities, and how to get quantifiable measurements of the benefits.


Downloadable resources


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java development
ArticleID=1061638
ArticleTitle=Class sharing in Eclipse OpenJ9
publish-date=06062018