Shared classes cache now supports fat jar

Recently OpenJ9 extended the class sharing feature to include fat jars (issue here). Starting from v0.9.0, applications running with fat jar, like Spring Boot, will now also benefit from the faster start-up time and reduced memory footprint from the class sharing feature of Eclipse OpenJ9.

OpenJ9 class sharing

The class sharing feature of Eclipse Openj9 can be enabled by running the JVM with the -Xshareclasses option specified on the command line. Bootstrap classes, extension classes, and application classes, as well as Ahead of Time (AOT) compiled code, are stored into a class cache that can be shared across multiple JVMs.

When the JVM loads a class from disk, it converts the read-only part of the Java class into an internal data structure called J9ROMClass. Unless the Java class is modified and recompiled using javac, such read-only information won’t change from run to run, and can be shared across multiple JVMs. Thus, there is no need for each JVM to repeat the process of loading the class from disk and creating the same J9ROMClass. With shared classes enabled, the first JVM creates J9ROMClasses and stores them into a shared cache on the system. Subsequent JVMs can directly load classes from the shared classes cache, which results in a faster startup time. In tandem with AOT, startup time can be improved by as much as 40% with -Xshareclasses enabled (More details can be found here).

With no shared classes cache, the JVM uses the following class look-up order by default:

  1. Classloader cache
  2. Parent
  3. Filesystem

When shared classes cache is enabled, the class look-up order becomes:

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

Since the classes in the cache are shared by multiple JVMs, the memory footprint is reduced if there are multiple JVMs running on your system. Moreover, AOT will be activated when class sharing is enabled. An introduction to AOT can be found here: Intro to Ahead Of Time Compilation.

Shared classes support for fat jar

Any class loader that extends java.net.URLClassLoader (and jdk.internal.loader.BuiltinClassLoader in Java 9 and up) is able to find/store classes from/to the shared cache. Previously, OpenJ9 class sharing supports only classes loaded from traditional jar, zip and class files (and Jimage in Java 9 and up). Now Openj9 class sharing supports fat jar (i.e. a jar inside another jar), which means Openj9 is able to provide memory footprint savings and a faster startup time to applications running with fat jar.

After starting up your application using the -Xshareclasses option on the command line, the classes from the fat jar will be stored to the shared cache. You can specify the size and name of the shared classes cache using -XX:SharedCacheHardLimit= and -Xshareclasses:name=.  More details of the shared classes command line options can be found here.

You can use java -Xshareclasses:name=<name>,printStats=classpath to find the fat jar on the class path. The jars inside spring-petclinic-2.0.0.jar are displayed in the following example. The option also gives you basic statistics of the shared cache.

java -Xshareclasses:name=Cache1,printStats=classpath
Current statistics for cache "Cache1":
...
1: 0x00007F723BAC45A8 CLASSPATH
        /team/hangshao/Test/spring-petclinic-2.0.0.jar!/BOOT-INF/classes
        /team/hangshao/Test/spring-petclinic-2.0.0.jar!/BOOT-INF/lib/spring-boot-starter-actuator-2.0.0.BUILD-SNAPSHOT.jar
        /team/hangshao/Test/spring-petclinic-2.0.0.jar!/BOOT-INF/lib/spring-boot-starter-2.0.0.BUILD-SNAPSHOT.jar
        /team/hangshao/Test/spring-petclinic-2.0.0.jar!/BOOT-INF/lib/spring-boot-starter-logging-2.0.0.BUILD-SNAPSHOT.jar
...

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

Cache contains only classes with line numbers

base address                         = 0x00007F7238E56000
end address                          = 0x00007F723C000000
allocation pointer                   = 0x00007F723A987418

cache size                           = 52428192
softmx bytes                         = 52428192
free bytes                           = 11705696
ROMClass bytes                       = 28513304
AOT bytes                            = 5698068
Reserved space for AOT bytes         = -1
Maximum space for AOT bytes          = -1
JIT data bytes                       = 149328
Reserved space for JIT data bytes    = -1
Maximum space for JIT data bytes     = -1
Zip cache bytes                      = 1131696
Data bytes                           = 351648
Metadata bytes                       = 712820
Metadata % used                      = 1%
Class debug area size                = 4165632
Class debug area used bytes          = 3388002
Class debug area % used              = 81%
stale bytes                          = 0

# ROMClasses                         = 10907
# AOT Methods                        = 2592
# Classpaths                         = 6
# URLs                               = 0
# Tokens                             = 0
# Zip caches                         = 21
# Stale classes                      = 0
% Stale classes                      = 0%

Cache is 77% full

Cache is accessible to current user = true

A shared classes cache persists beyond the lifetime of the JVM. It is possible that Java classes are modified, re-compiled and re-added to the fat jar. In this case, it is incorrect to load stale classes from the shared cache. OpenJ9 handles this case safely by storing the timestamp of the file from which the class is loaded. The JVM is able to detect the timestamp change of the fat jar file (spring-petclinic-2.0.0.jar in the above example) to make sure stale classes won’t be returned from the shared cache. Unfortunately, the JVM does not know which class in the jar is actually updated, so it will pessimistically mark all the classes from that fat jar as stale. When the class is re-loaded from disk, it is compared against the one in the shared cache. If it is not changed, the one in the shared cache will be immediately marked as non-stale and it is able to be loaded by subsequent JVMs again. If it is changed, the new one will be stored into the shared cache.

You can find all the stale classes in the shared cache using java-Xshareclases:name=,printStats=stale. You can also see the total number and percentage of stale classes in the cache statistics.

java -Xshareclasses:name=Cache1,printStats=stale 

Current statistics for cache "Cache1":
...
1: 0x00007FB0AACDCF94 ROMCLASS: org/springframework/data/domain/Persistable at 0x00007FB0AA04D400.!STALE!
        Index 42 in classpath 0x00007FB0AB28BA14
1: 0x00007FB0AACDCF60 ROMCLASS: org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation at 0x00007FB0AA04D540.!STALE!
        Index 41 in classpath 0x00007FB0AB28BA14
...
# ROMClasses                         = 19621
# AOT Methods                        = 2555
# Classpaths                         = 9
# URLs                               = 0
# Tokens                             = 0
# Zip caches                         = 5
# Stale classes                      = 8244
% Stale classes                      = 42% 

Cache is 77% full 

Cache is accessible to current user = true

Coming next

Eclipse OpenJ9 is enabling the class sharing feature by default. Users will benefit from the faster start-up time and memory footprint saving of OpenJ9 without specifying -Xshareclasses on the command line. This work is currently in progress (issue here), so please watch this space.

1 Reply to “Shared classes cache now supports fat jar”

Leave a Reply