Improve Launch Times On Java 10 With Application Class-Data Sharing

Surely the most prominent Java 10 feature is type inference with var, but there’s another gem hidden in 10 that’s worth exploring: application class-data sharing (AppCDS). It allows you to reduce launch times, response time outliers, and, if you run several JVMs on the same machine, memory footprint. Here’s how!

Overview

This post is part of a short series about Java 10. I created a demo project for all features on GitHub, which also explains how to set up JDK 10. Check it out if you want to play with the new features yourself.

The demo project contains a script showing off application class-data sharing. AppCDS is an extension of the commercial class-data sharing (CDS) feature that Oracle’s JVM contains since Java 8. If you’re interested in a little more background, check out JEP 310, which generalized CDS to AppCDS.

Note that you need OpenJDK 10 to experiment with AppCDS because Oracle JDK only contains CDS and the option -XX:+UseAppCDS won’t work. Unfortunately, Oracle’s OpenJDK 10 builds do not contain JavaFX, so you will have a hard time applying AppCDS to a JavaFX application.

Application Class-Data Sharing In A Nutshell

To execute a class’ bytecode, the JVM needs to perform a couple of preparatory steps. Given a class name, it looks the class up in a JAR, loads it, verifies the bytecode, and pulls it into an internal data structure. That takes some time of course, which is most noticeable when the JVM launches and needs to load at least a few hundred, more likely a couple of thousand classes.

The thing is, as long as the application’s JARs do not change, this class-data is always the same. The JVM executes the same steps and comes to the same result every time it runs the app.

The idea behind AppCDS is to create a class-data archive once and then share it, so that the JVM need not recreate it

Enter application class-data sharing! The idea behind it is to create this data once, dump it into an archive, and then reuse that in future launches and even share it across simultaneously running JVM instances:

  1. create a list of classes to include in the archive (possibly with -XX:DumpLoadedClassList)
  2. create an archive with the option -Xshare:dump
  3. use the archive with the option -Xshare:on

When launching with -Xshare:on, the JVM maps the archive file into its own memory and thus has most classes it needs readily available and does not have to muck around with JARs. The memory region can even be shared between concurrently running JVM instances, which frees up memory that would otherwise be wasted on replicating the same information in each instance.

AppCDS significantly reduces the time the JVM has to spend on class loading

AppCDS significantly reduces the time the JVM has to spend on class loading, which is most noticeable during launch. It also prevents long response times in the case where a user is the first to access a feature that requires a lot of classes that weren’t loaded yet.

Working With A JDK Class-Data Archive

The simplest way to get started with class-data sharing is to limit it to JDK classes, so we’ll do that first. We will then observe that a simple “Hello, World” JAR can be launched in almost half the time.

Creating A JDK Class-Data Archive

The JRE already comes with a list of classes, which -Xshare:dump uses by default, so we can go straight to step 2 and generate the archive:

You might have to execute this command with admin rights because by default the JVM creates the archive file classes.jsa in ${JAVA_HOME}/lib/server. On Linux, the resulting file is about 18 MB.

Using A JDK Class-Data Archive

Just as -Xshare:dump creates the archive in a default location, -Xshare:on reads from that same default, so using the archive is pretty simple:

We can use unified logging with -Xlog to observe CDS in action by analyzing the class loading log messages:

The file cds.log then contains messages like the following:

As you can see, Object was loaded from the “shared objects file”, which is another term for the archive, whereas HelloAppCDS isn’t. A simple count of lines shows that for an app with a single class 585 classes are loaded from the archive and 3 aren’t.

There are always some classes whose data can not be archived

That implies that some JDK classes could not be loaded from the archive and that’s exactly right. There are some specific conditions under which it is not possible to archive a class’ data, so the JVM won’t embed it and will instead end up loading it at run time as usual.

Launch Time Measurements

A crude way to measure the performance improvement is to simply time the execution of the entire demo app while toggling -Xshare between off and on:

The interesting bit is real, which gives the wall-clock time it took to run the app. Without class-data sharing those numbers vary between 70 ms and 85 ms (need I add “on my machine”?), with sharing they land between 40 ms and 50 ms. The exact numbers don’t really matter, but you can see that there’s quite some potential.

Note, though, that this is the maximum performance gain you are going to get for a JDK archive. The more classes the application comes with, the lower becomes the share of JDK classes and hence the relative effect of loading them faster. To scale this effect to large applications, you need to include their classes in the archive, so let’s do that next.

Working With An Application Class-Data Archive

To actually share data for application classes as opposed to just JDK classes, we need to activate AppCDS by adding the command line switch -XX:+UseAppCDS (remember, public downloads of Oracle JDK 10 don’t include this feature). Creating and using an archive that includes application class-data follows the same logic as for JDK classes.

Creating A List Of Application Classes

This time we can not skip the step to come up with the list of classes to include in the archive. There are at least two ways to create that list: You can either do it manually or ask the JVM to do it for you. I’ll tell you about the latter and once we have a file in hand, you will see how you might have generated it by hand.

To have the JVM create the list, run the application with the -XX:DumpLoadedClassList option:

The JVM will then dutifully record all loaded classes. If you want to include just the classes you need to launch, exit the app right after that. If, on the other hand, you want to include classes for specific features, you should make sure they are used at least once.

In the end you get a file classes.lst that contains the slash separated name of each class that was loaded. Here are the first few lines:

As you can see, there’s nothing magical about this file and you could easily generate it by other means.

Creating An Application Class-Data Archive

The second step is to actually create the archive. We do that much like before but have to mention the list of classes with -XX:SharedClassListFile. And because this archive is application-specific, we don’t want it in ${JAVA_HOME}, so we define the location with -XX:SharedArchiveFile:

Note that we do not launch the application with -jar. Instead we just define the class path with all the JARs the application needs. We will see in a minute why the path’s details are of the utmost importance and also why you can’t use wildcards like lib/* or exploded JARs like target/classes.

Depending on the size of the class list, this step might take a while. When it’s done, your archive, in this case app-cds.jsa, is ready to be used.

Using An Application Class-Data Archive

Using the is pretty straightforward. We need to unlock AppCDS, activate sharing and point to the archive:

If we analyze the class loading log messages as before, we see that with the application class HelloAppCDS could be loaded from the archive:

Heed The Class Path Content

What are the two biggest challenges in software development?

  1. naming
  2. cache invalidation
  3. off-by-one errors

The class-data archive is a cache

Cheap joke, I know, but relevant because the class-data archive is essentially a cache and so we need to ask ourselves under which circumstances it becomes stale and how that can be detected.

It is obviously a problem if a JAR was replaced and now contains classes that differ from the archive’s (say, someone drops an updated dependency into the app’s lib folder). And because the class path is always scanned linearly, even just reordering the same artifacts can change the app’s run-time behavior. In summary, the archive is no longer a correct representation of the class path, if the latter doesn’t contain the same artifacts in the same order as when the archive was created.

The class path used to launch the app must have the archive’s path as a prefix

To have a decent chance at detecting a class path mismatch, the JVM embeds the string that lists the class path content in the archive when creating it. When you launch the app with the archive, the JVM compares the actual class path with the embedded one and only launches if the latter is a prefix of the former.

That means the class path used to launch the app must first list all elements of the archive’s path in the same order, but can then append more JARs as it sees fit. This is of course by no means fool proof, but should detect a decent chunk of problematic situations.

The more specific the class path that is used to create the archive, the more reliably can it be used to “invalidate” the cache / archive. And that’s why you can use neither wild cards nor exploded JARs when creating the archive.

(If you look closely, you will notice that in my examples, I create the archive with --class-path app.jar but launch without class path because I use -jar app.jar. Wait, a non-empty class path can hardly be the prefix on an empty class path. It works nonetheless because the JAR specified with -jar is implicitly added to the class path.)

We’ve been talking an awful lot about the class path – what’s with the module path? Can you put code from JPMS modules into the archive? Unfortunately, not – from the JEP:

In this release, CDS cannot archive classes from user-defined modules (such as those specified in --module-path). We plan to add that support in a future release.

Launch Time Measurements

For a simple “Hello, World” application there is of course no performance boost of AppCDS over CDS because loading one class more or less from the archive has no measurable impact. So I took application class-data sharing for a ride with a large desktop application.

I focused on the launch, which loads about 25’100 classes and takes about 15 seconds. During that time, there’s a lot more going on that just fetching classes, but at least there are no network operations to skew the results.

The archive contained 24’212 classes (so about 900 could not be included) and has roughly 250 MB. Using it to run the application brought the launch time down by about 3 seconds (20 %), which I felt quite good about. Your mileage will vary, of course, so you have to do this yourself to know whether it’s worth the effort for your app.

Reflection

There are three steps to creating and using an archive with application class-data:

  1. Creating a list of classes to include in the archive:

  1. Creating an archive:

  1. Using the archive:

Keep in mind that the class path used to launch the application must have the one used to create the archive as a prefix and that you can’t use wildcards or exploded JARs for the latter. Your launch time improvements depend a lot on your application, but anything between a couple and almost 50 % is possible. Finally, if you have any problems, use -Xlog:class+load to get more information.

Share & Follow

You liked this post? Then share it with your friends and followers!
twittergoogle_plusredditlinkedin
And if you like what I'm writing about, why don't you follow me?
twittergoogle_plusrssmail

Other Posts