Java provides many out-of-the box properties for configuring the JVM, especially in the areas of memory management and garbage collection (GC).
By tweaking the default JVM garbage collection settings, Credera was able to support a scalable Docker eCommerce web application where each image could handle 3,500 requests per minute with less than 1% CPU time dedicated to garbage collection.
Every application is different, so this blog post aims to provide an overview of the profiling tools and analysis we used in configuring our web application’s JVM rather than providing silver-bullet numbers. We hope our insights can then be applied to your own work and improve your application’s stability.
GARBAGE COLLECTION INTRODUCTION
There are many resources available that discuss the JVM garbage collection in detail, with some of the more important articles by Oracle (GC overview; new G1 overview). A full recounting of JVM garbage collectors is out of the scope of this blog post, but they all work on similar principles:
The JVM memory is divided into young, old, and permanent generations.
a. GC on the young generation is quick.
b. GC on the old generation is slow.
c. The permanent generation is never collected.
When an element is created on the JVM heap space, it is allocated to the young generation for new items.
As an element is used, it can be promoted out of the young generation and into the old.
When the young generation is full, a minor GC is performed that will clean only the young memory.
When the old generation is full, a major GC is performed that will clean both the young and old memory.
All GCs (minor and major) stop the JVM threads and block the application, so tuning the JVM to reduce the blocks is important.
In order to analyze our eCommerce application’s garbage collection and memory, we used two free tools: VisualVM and Java Mission Control.
VisualVM is an all-purpose JVM performance-tuning tool that provides real-time statistics for a JVM application.
We used VisualVM to easily see a real-time view of our garbage collection through a tab called VisualGC. This tab showed us the young memory (Eden and survivor) spaces, the old memory that was being used, and the amount of permanent metaspace we needed to allocate at the start of our application (Figure 1).
By analyzing these parameters in real-time, we also were able to see how quickly our application responded under load and if the old space grew from minor code or configuration tweaks. From this analysis, we saw that spikes of users could quickly strain an application with a small heap to force more GCs, but a maximum heap of 4GB could easily handle the load we anticipated per image.
VisualVM can also provide a larger view into how an application’s memory and garbage collection performs over time (Figure 2). Some important GC information we discerned from these result included how many classes loaded for the application during endurance testing (20,000 in our case), how high the CPU spiked from GC activity, and how the heap size grew as the number of users scaled.
JAVA MISSION CONTROL
Java Mission Control is an advanced JVM profiling tool provided by Oracle. Whereas VisualVM excels as a snapshot of an application, Mission Control works best when profiling an application.
In Mission Control, these profiles are done through Java Flight Recorder and can be kicked off through the Mission Control application or on the profiled app’s startup via Java configurations. After a record is finished, the garbage collection results provide a detailed analysis (Figure 3).
Some of the results that can be analyzed are:
The number of garbage collections during the profiling.
The average pause time during the profiling.
How many young and old collections were performed.
The total heap size used.
Mission Control can also save the profiling records for easy comparison later, so it’s simple to make some configuration changes in the JVM and see how the GC performance changes.
Using Mission Control and Flight Recorder in conjunction with load testing, we were able to execute consistent and predictable profiling reports. This helped us narrow down our choice of garbage collectors to the G1 (or garbage-first) collector. The G1 collector worked well for our web application since we wanted to reduce any large CPU spikes and thread blocks. Although the G1 collector had more collections than some of the other garbage collectors we tested, each collection took a small amount of time, which reduced thread blocks.
Besides the choice of garbage collector, we altered some other JVM memory and GC settings based on our analysis through VisualVM and Mission Control.
-XX:METASPACESIZE =##M -XX:MAXMETASPACESIZE=##M
These two configurations control the starting and maximum memory to use for the garbage collection’s metaspace, which is the permanent space that will never be collected. Setting the starting memory to what the application needs immediately prevents the application from performing unnecessary requests for more memory.
Strings are stored by the JVM to prevent constant String recreation. By setting the table size in the JVM, the memory necessary for the Strings can be saved without needing to grow the table again.
Along with the String table size and the metadata space, setting the number of classes that will be stored will reserve some memory immediately and prevent CPU and GC pauses as the class memory is allocated.
-XX:+USEG1GC OR -XX:+USEPARALLELGC OR -XX:+USECONCMARKSWEEPGC
These settings specify what garbage collector the JVM should use.
The default garbage collector in Java 8 is the Parallel GC, where multiple threads garbage collect simultaneously to provide high throughput. A new garbage collector, G1 (or garbage-first), was introduced in Java 7 and is aimed to reduce GC pause times while retaining a high throughput. G1 is being considered as the default garbage collector in Java 9.
This specifies the maximum number of milliseconds you would like the garbage collector to pause. This is a suggestion to the garbage collector and cannot be strictly enforced. A lower setting indicates more collections but less time during each collection, whereas a higher setting indicates less collections but longer pause spikes.
-XX:+PRINTGCDETAILS -XX:+PRINTGCDATESTAMPS -XLOGGC:<FILENAME>.LOG
This setting outputs some garbage collection details that can be analyzed. This is helpful if you don’t have access to use some of the GC tools in an environment.
-XX:+UNLOCKCOMMERCIALFEATURES -XX:+FLIGHTRECORDER -XX:+UNLOCKDIAGNOSTICVMOPTIONS – XX:+DEBUGNONSAFEPOINTS -XX:STARTFLIGHTRECORDING=DURATION=##S,FILENAME=<FILENAME>.JFR
This set of configuration options starts a flight recorder setting immediately on application startup. By setting a recorder duration and a file to log, the application can have a reliable and reproducible way of checking how different GC configuration options affect an application’s performance.
Understanding the JVM’s garbage collection can improve your production application’s performance by reducing GC pause times that stop thread execution.
In this blog post, we introduced some tools to help you discover the best configuration for your application. These tools and analysis helped us create a stable eCommerce application, and we hope they can assist you in your next Java endeavor.