New! JVM Inventory, a feature of Azul Intelligence Cloud, accelerates Oracle Java migration and ensures ongoing compliance - Learn More
Support
Blog chevron_right Java

How ReadyNow Improves Java Warmup Time

How ReadyNow Improves Java Warmup Time

This is the second blog post in a series on faster Java application warmup. The first blog post, Faster Java Warmup: CRaC versus ReadyNow, explained how CRaC and ReadyNow use different methods to achieve faster Java warmup. This post takes a deeper look into how ReadyNow improves Java warmup time and reduces latency.

ReadyNow in Azul Zing Builds of OpenJDK (Zing) helps Java applications reach their optimal performance much faster after the startup of the JVM. Using a text file containing the history of compiler decisions from a previous run, it can reduce the application’s total Java warmup time.

In this post, we’ll take a deeper look into this process and explain how ReadyNow can improve the speed of the application from the start and reduce latency [Figure 1].

CHART - Java warmup time with ReadyNow
Figure 1: ReadyNow improves Java warmup time

How ReadyNow achieves faster compilations

Typically, for a method to get compiled from Java bytecode into native code, it needs to be executed multiple times. The threshold is different for the Tier 1 and Tier 2 compilers, the runtime being used, and the optional configurations, but typically it’s above 5,000 or 10,000 times. All those executions take time, and the actual production load is required.

With ReadyNow, the JVM knows from a previous run that these methods were compiled and which decisions the compiler has made to achieve the optimal native code. Therefore, it will start the compilation proactively and not wait for all the thresholds to be passed. Thus, the compilation begins sooner, saves time, and reaches the warmed-up state faster.

Read Now
About ReadyNow
ReadyNow Start Up Faster and Stay Fast
High-Performance Java Platform Benchmark Report

Normal startup

The first step in using ReadyNow is running your application as you would normally do, with the Zing JVM and the extra argument

-XX:ProfileLogOut=<file>

This instructs Zing to store (output) each compiler decision in the profile log file.

This example also uses the argument to generate garbage collector logs to compare the differences further in this article. You can use your own application, of course, but make sure you have some way to trigger the expected load the application will need to handle in production. In this case, I’m using the DaCapo Benchmark Suite, which can be instructed to self-generate a specific load, enabling us to compare different runs with a similar load easily.

This is just an example use. In an ideal case, you would use your production application with the actual load it needs to handle. You would also need to let it run longer to ensure that you end up with a profile log with all the required information based on the application’s real-world use from startup to the best performing point.

$ /usr/lib/jvm/zing-jdk21/bin/java \
   -XX:ProfileLogOut=readynow.log \
   -Xlog:gc:gc.log \
   -jar dacapo-23.11-chopin.jar h2

What’s in the file

The generated profile log-out file contains log lines useful for you as a developer or DevOps by checking the content in the file itself or with GC Log Analyzer. For instance, you can check this file for the Java version, arguments, and other runtime information. There are also records in the file for loaded classes and compiled methods related to how the application behaves during warmup, so ReadyNow can proactively help the JVM finish the warmup sooner when this file is used as the input profile log.

# ZVM version:
# 21.0.5.0.101-zing_25.01.0.0-b5-release-linux-X86_64
# VM Arguments:
# jvm_args: -XX:ProfileLogOut=readynow.log -Xlog:gc:gc.log 
# java_command: dacapo-23.11-chopin.jar h2
# class_path: dacapo-23.11-chopin.jar
# java_home: /usr/lib/jvm/zing-jdk21.0.5.0.101-25.01.0.0-5-amd64
...
# ProfileLogOut after macro expansion: 'readynow.log'
...
# GCHH : OS name       : Linux 
# GCHH : OS release    : 6.8.0-53-generic #55-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 17 15:37:52 UTC 2025 
# GCHH : hardware      : x86_64 
# GCHH : CGroups information: 
# GCHH :   Container Type                   : cgroupv2 
# GCHH :   Active processor count           : 16 
...
1740141511794777793 | FirstCall 8512 5 0
Class 877 1260777173 org.dacapo.harness.TestHarness 9 { } 272310769 Class 901 file:/dacapo-23.11-chopin.jar
1740141511795023287 | ClassLoad 877
ImplementorLoad 877 1 877 

In some cases, Azul support engineers also use this file to assist customers in debugging specific performance, compilation, and warmup issues. Application performance graphs (response time, throughput) can be combined with these profile logs to compare the results with and without ReadyNow. Based on the metrics that are important for the customer, it can help decide if the same system performance can be achieved with fewer nodes in a cluster, for example.

Startup with file

In the second step, we start the same application, but we use the profile generated in the first step and use it this time with the -XX:ProfileLogIn=<file> argument (input). This instructs Zing to start compiling the Java class files into native code immediately based on the decisions made by the compiler in the first run.

$ /usr/lib/jvm/zing-jdk21/bin/java \
   -XX:ProfileLogIn=readynow.log \
   -Xlog:gc:gc-with-profile.log \
   -jar dacapo-23.11-chopin.jar h2

Comparing the different runs

The gc.log files generated by Zing contain much more information, especially about the compiler’s behavior, than similar files generated by an OpenJDK build. Therefore, we can conclude how the compiler worked during the run. Such detailed information is not available with other OpenJDK runtimes.

Let’s compare gc.log files generated during the two runs with GC Log Analyzer.

Tier 1 compile counts

In the first run, we can see the number of compiles gradually grows over time and reaches the maximum after about 40 seconds. Thanks to the information in the profile log file, in the second run, the compiler already has all the required information to move these Tier 1 compilations to the very start of the application and finish all these compilations in one second.

Tier 2 Compile Counts

The charts for the Tier 2 compilation reveal that we reduced a significant part of the compilations from 40 seconds to 17 seconds. Although this is already a significant improvement, it also helps us to define that the generated profile log file is still not perfectly trained for this use case. In one of the next parts of this blog series, we’ll explore the topic of generational profile logs further.

Compiler queue run

The chart with the code waiting in a queue to be compiled also shows a big difference. In the first run, we can see a huge spike starting at around second 26, when the actual Dacapo-test starts, which causes a lot of load on the application. Thanks to the profile log, this queue is moved to the start of the application. As described before, ReadyNow uses the decisions of the previous run to immediately compile most of the native code and have it available when needed. As a result, we only see a small peak at 26 seconds.

Conclusion

Using a profile log generated during a “training run” of the application, we can move the compilation from Java bytecode to the best-performing native code at the very start of the application. This leads to very fast warmup of new instances of an application, which helps you dynamically scale your system and perform at maximum speed quickly after startup.
Next: How to Train ReadyNow to Achieve Optimal Performance