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.
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 primary/secondary 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 classes are 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:
- Classloader cache
In contrast, a JVM running with the class sharing feature uses the following order:
- Classloader cache
- Shared classes cache
Classes are read from and written to the shared classes cache using the public Helper API. The Helper API is integrated into
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
RAMClassthat contains mutable data, such as static class variables
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
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
-Xscmaxaot<x> to set the size of AOT space in the shared cache. If neither
-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
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
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
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
printStats option prints summary statistics on the named cache, as Listing 3 shows.
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.
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.
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 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.
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 enabledJVMSHRC236I 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 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 enabledFailed 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 enabledFound 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. … …
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 enabledInfo 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
-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.
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
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
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
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.
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
When deploying class sharing with your application, you need to consider factors such as security and cache tuning. These considerations are briefly summarised here.
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
-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.
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.
- Download JDK with OpenJ9 from the Adopt OpenJDK project or pull from the docker image.
- Download shcdemo.jar from GitHub.
- Run the test a couple of times without class sharing to warm up the system disk cache, using the command in Listing 11: When the window in Figure 1 appears, press the button. The app will load the classes.
Listing 11. Warming up the disk cache
C:\OpenJ9>wa6480_openj9\j2sdk‑image\bin\java ‑Xshareclasses:none ‑cp shcdemo.jar ClassLoadStress
Figure 1. Press the buttonOnce 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.
- 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. You can also check the cache statistics use printStats, as Listing 13 shows:
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 enabledJVMSHRC236I 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.
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
- 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: 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.
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 enabledJVMSHRC237I 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.
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.
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
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.