Road to a fast(er) Java CLI, part 1: AppCDS
Speeding Up a Java CLI Part One: AppCDS

    

During my free time, I am developing javaimports
,
a goimports-like Java command line tool that auto-imports Java classes without relying on an IDE, a LSP or
any kind of cache. I use it daily to write Java, pairing it with google-java-format to
automatically format my code and add missing imports on save. Obviously, I need
javaimports to run fast, which pushes me to look for ways to make my code, and Java command line
applications in general, faster.
My first move was to add multithreading support. But when I ran a poor man’s benchmark using time,
some numbers caught my attention.



project
number of files
dependencies
v1.1 (single thread) time
v1.2 (multi thread) time




A
444
29
2.4s
1.6s


B
90
9
1.2s
1.0s


C
4
0
0.6s
0.6s



Unsurprisingly, multithreading does not help in project C. But 0.6 seconds to parse 4 files and scan
0 dependency sounds like an awful lot of time to me! A quick analysis found a bottleneck: despite
all files being roughly the same size, parsing the first file took close to a hundred times longer
(200ms as opposed to 2~3ms).
I first suspected there might be some kind of caching involved, but the library I used did not
contain any. What else could explain why the first time is abnormally slow? My suspicions turned
to class loading. Running java with -Xlog:class+load did show something interesting around the
first call to Parser:
[0.108s][info][class,load] com.nikodoko.javaimports.parser.Parser source: file:/Users/nicolas.couvrat/javaimports-1.2-SNAPSHOT-all-deps.jar
...
~1100 classes!!
...
[0.313s][info][class,load] com.nikodoko.javaimports.parser.Parser$$Lambda$73/0x0000000800c47040 source: com.nikodoko.javaimports.parser.Parser
The large number of classes involved suggested that improving loading time could lead to serious
performance gains. I decided to look at what happens when the Java Virtual Machine (JVM) loads a class.
What Happens When the JVM Loads a Class?
As you probably know, Java is a hybrid language, halfway between compiled languages (like C) and
interpreted languages (like Python). Java code is first compiled to bytecode, which is the machine
language of a virtual machine called the JVM. The JVM then interprets it1, which means that
bytecode is loaded at runtime.
Most of the details of class loading are out of the scope of this article, so I will stick to a
broad overview. If you wish to learn more, I recommend you check Oracle’s very thorough
documentation
.
Loading Bytecode And Creating an Internal Representation
When the JVM first encounters a class name, it will try to load it using an available class loader. A
class loader can be viewed as a bytecode provider: most of the time, that bytecode will come from a
.jar archive stored locally, but it could occasionally be downloaded over the network, generated
on the fly, etc.
The JVM will parse that binary data and, provided it represents a valid .class
file
, derive an internal
representation from it. That representation will eventually be stored in the method area of the JVM
(a section of the heap shared by all JVM threads), and used every time the corresponding class is
initialized or called by the rest of the code.
Linking
Before using that internal representation, however, the JVM goes through a number of additional
steps, together referred to as “linking”.

it verifies that the bytecode satisfies a number of constraints
,
it rewrites parts of it, optimizing what it can,
it resolves it by loading and creating the other classes it uses (also checking access
permissions),
and etc.

Initialization
Finally, static fields and static blocks are initialized: the class is now ready to be used.
As you can see, that’s a lot of work, especially given that it’s done recursively, and for all the core
classes too. And because it is only performed when a class is first encountered, most of the price
is paid at startup. While this is acceptable for a long running, server-side application, it is very
painful for a short-lived command line tool that will have to go through this loading step every
single time.
Reducing Startup Time With AppCDS
Maintainers of the Java language have long been aware of this issue, and have introduced Class Data
Sharing (CDS) as a way to mitigate it. A first version was included with Java SE 5.0, limited to a
small set of core classes. It was improved and extended several times, with AppCDS (that also works
for user-defined classes) being finally released in OpenJDK 102.
The idea is simple. The internal representation of loaded classes is dumped in an archive, that can
be directly memory-mapped the next time the application runs, removing the need for expensive
verification and parsing. This reduces the memory footprint (as part of the class archive can be
shared between JVM processes, and less allocations are made), but also obviously helps with speed,
which is what I am interested in here.
The original iteration of AppCDS used to require 3 steps to generate and use the archive file3, but
it has been simplified with OpenJDK 13. It is now a matter of running the following 2
commands:
# Run the program once to generate a shared archive file
java -XX:ArchiveClassesAtExit=cds.jsa -jar ...

# Then use it every other time and profit!
java -XX:SharedArchiveFile=cds.jsa -jar ...
How efficient is it at decreasing loading time? Using -Xlog:class+load shows the following:
[0.104s][info][class,load] com.nikodoko.javaimporthets.parser.Parser source: shared objects file (top)
...
still ~1000 classes, but a lot of them now come from the shared objects file
...
[0.222s][info][class,load] com.nikodoko.javaimports.parser.Parser$$Lambda$74/0x000000080160cc40 source: com.nikodoko.javaimports.parser.Parser
With AppCDS, the first call to Parser takes 100ms less! Overall, running javaimports on the
small project C now takes 400ms instead of 600ms, a 33% speed increase. Of course, the gain is flat:
it only affects class loading, and the amount of classes loaded does not change depending on how
long the program runs. In my case, on the bigger project A, the same 200ms difference can be
observed, going from 1.6s to 1.4s: less impressive, but still a solid 13% improvement.
So, do you want to use AppCDS in your project? For a short-lived Java application not spending too
much time waiting on I/O, the answer is probably yes. It’s hard to predict exactly how much it will
benefit you, but hoping for a 30% improvement in startup time for a small command line utility is
not unreasonable. As for me, I’m not yet entirely satisfied. It’s not bad, but can’t it go even
faster? (Spoiler: The answer is yes, and I will cover it in a next article).
As always, shoot me a message or tweet @nicol4s_c
 if you want to
chat about any of this, if you spotted any mistakes or typos, or if you’d like me to cover anything
else! Have a great day :)




Of course, modern day JVMs pack plenty of features and optimizations like just-in-time compilation, but these are mostly irrelevant to code that is executed a low number of times like it is the case in command line applications. ↩︎


See JEP 310
 for more details. ↩︎


See the JEP 310, or alternatively this short blog post
. ↩︎




  