Do you want to improve the start-up and warm-up times of your Java applications? Maybe the CRaC project can help you.
Luan Barbosa Ramalho
Posted on February 17, 2024
Hey guys, how are you?
I think you've heard that Java applications start slowly, haven't you?
Well, let's remember how does Java programming language works. Firstly, we write our code in plain text in files with the extension .java, so the javac compiles the code into .class files that are not code native for our processors, but, for the JVM (Java Virtual Machine) that interacts with our processors generating native code.
This entire process takes longer than a native program. However, it is important to say that the JVM is capable of making optimizations in our code based on the behavior of our application.
These optimizations can be done with the C1 Compiler, which is fast but generates simpler optimized code, or with the C2 Compiler, which is slow but generates more sophisticated optimized code.
Whenever the Java application is executed, the JVM starts its improvements to our code, however, during this process and analyzing the behavior of our application, the virtual machine understands that some improvements no longer make sense and starts a rollback of these optimizations, this process is called deoptimization.
As we have more deoptimizations and optimizations shortly after starting the application, a drop in performance is normal. This time is known as the warm-up period.
With the aim of seeking possible solutions to some of the problems with the start-up and warm-up times, there is a project in progress called CRaC (Coordinated Restore at Checkpoint). So far, the use of CRaC depends on the Linux project CRIU (Checkpoint/Restore In Userspace) therefore, it is only available for Linux.
The expected behavior using CRaC is that at a certain point during the application's execution, we will perform a checkpoint and an image will be created with the state of our application. Afterwards, we will restore to the checkpoint and our application should start with the same previous state.
NOTE
It is necessary to close all open files, sockets, etc.
For example, it may be impossible to store sockets in the checkpoint image.
Let's see below an experimental code using CRaC.
To run this example I used Linux on an Ubuntu distribution
Ubuntu 22
and an implementation of Azul JDK 21 with CRaC support
openjdk 21.0.1 2023-10-17 LTS
OpenJDK Runtime Environment Zulu21.30+19-CRaC-CA (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Zulu21.30+19-CRaC-CA (build 21.0.1+12-LTS, mixed mode, sharing)
CRaC provides an interface called Resource with two methods beforeCheckpoint and afterRestore. We will implement when it is necessary to take some action before the checkpoint or after the restoration. We will see an example of its use below.
package jdk.crac;
public interface Resource {
void beforeCheckpoint(Context<? extends Resource> context) throws Exception;
void afterRestore(Context<? extends Resource> context) throws Exception;
}
Let's create a class called WarmUp with two lists, one that assumes something important for our application after restoration and the other that doesn't.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class WarmUp {
public List<String> somethingImportant = Collections.synchronizedList(new ArrayList<>());
public List<String> somethingUnnecessary = Collections.synchronizedList(new ArrayList<>());
}
Now, let's see our the Resource implementation
import jdk.crac.Context;
import jdk.crac.Resource;
public class MyCRaCResource implements Resource {
private WarmUp warmUp;
public MyCRaCResource(WarmUp warmUp) {
this.warmUp = warmUp;
}
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
System.out.println("BeforeCheckpoint important size - " + warmUp.somethingImportant.size());
System.out.println("BeforeCheckpoint unnecessary size - " + warmUp.somethingUnnecessary.size());
System.out.println("BeforeCheckpoint Clear unnecessary");
warmUp.somethingUnnecessary.clear();
System.out.println("BeforeCheckpoint after clear important size - " + warmUp.somethingImportant.size());
System.out.println("BeforeCheckpoint after clear unnecessary size - " + warmUp.somethingUnnecessary.size());
System.out.println("Finish beforeCheckpoint");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
System.out.println("AfterRestore important size - " + warmUp.somethingImportant.size());
System.out.println("AfterRestore unnecessary size - " + warmUp.somethingUnnecessary.size());
}
}
We will print before checkpoint the value of the two lists, clear the unnecessary list and after restoring print the values again.
In our Main class, we will fill out the lists and register our resource.
import jdk.crac.Core;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) throws InterruptedException {
var warmUpBean = new WarmUp();
var resource = new MyCRaCResource(warmUpBean);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 500).forEach(i -> {
executor.submit(() -> {
warmUpBean.somethingImportant.add("Important-" +i);
warmUpBean.somethingUnnecessary.add("Unnecessary-" +i);
return i;
});
});
}
Core.getGlobalContext().register(resource);
System.out.println("Register resouce - " + Core.getGlobalContext().toString());
for (int i = 1; i <= 1000; i++) {
System.out.println("i = " + i);
Thread.sleep(500);
}
}
}
When we implement the resource it is necessary to register it in the context Core.getGlobalContext().register(resource);
or the API won't know that it is necessary to call the resource methods beforeCheckpoint and afterRestore.
Now, let's build our application gradle build -Dorg.gradle.java.home=/home/luanbramalho/.sdkman/candidates/java/21.0.1.crac-zulu
We will create a directory for the application images when we do the checkpoint, in my case I created the crac-gradle-files directory in the same path as my application and declared it using the CRaCCheckpointTo java option.
java -XX:CRaCCheckpointTo=../crac-gradle-files -jar ./build/libs/crac-gradle-example-1.0-SNAPSHOT.jar
During execution, our application will do a for and print he index
i = 147
i = 148
i = 149
i = 150
i = 151
i = 152
i = 153
i = 154
i = 155
With our application running, let's do the checkpoint using the Java diagnostic command ( jcmd ) tool
jcmd /build/libs/crac-gradle-example-1.0-SNAPSHOT.jar JDK.checkpoint
After we run the above command, our application that was running should stop and print something as this output:
i = 154
i = 155
fev. 12, 2024 10:42:57 PM jdk.internal.crac.LoggerContainer info
INFO: Starting checkpoint
BeforeCheckpoint important size - 500
BeforeCheckpoint unnecessary size - 500
BeforeCheckpoint Clear unnecessary
BeforeCheckpoint after clear important size - 500
BeforeCheckpoint after clear unnecessary size - 0
Finish beforeCheckpoint
fev. 12, 2024 10:42:57 PM jdk.internal.crac.LoggerContainer info
INFO: /home/luanbramalho/Documents/crac-gradle-example/build/libs/crac-gradle-example-1.0-SNAPSHOT.jar is recorded as always available on restore
Killed
NOTE
I needed to run the application and commands as root on my operating system.
Now if we list the files in our checkpoint directory crac-gradle-files we should see our .img files:
/home/luanbramalho/Documents/crac-gradle-files# ls
core-6171.img core-6179.img core-6187.img core-6229.img pagemap-6171.img
core-6172.img core-6180.img core-6188.img dump4.log pages-1.img
core-6173.img core-6181.img core-6190.img fdinfo-2.img pstree.img
core-6174.img core-6182.img core-6191.img files.img seccomp.img
core-6175.img core-6183.img core-6225.img fs-6171.img stats-dump
core-6176.img core-6184.img core-6226.img ids-6171.img timens-0.img
core-6177.img core-6185.img core-6227.img inventory.img tty-info.img
core-6178.img core-6186.img core-6228.img mm-6171.img
The next step is to restart our application from our checkpoint images with the command java -XX:CRaCRestoreFrom=../crac-gradle-files
The output of the application should be something like below
# java -XX:CRaCRestoreFrom=../crac-gradle-files
i = 156
AfterRestore important size - 500
AfterRestore unnecessary size - 0
i = 157
i = 158
i = 159
i = 160
i = 161
i = 162
i = 163
i = 164
i = 165
As we can see, the restore continued the value of our index (i) and the afterRestore method in our resource was also executed keeping the values in the important list and no values in the unnecessary list.
Therefore, in a real application, we might need to perform some actions during our initialization, or some time with the application running after warming up to execute our checkpoint, thus allowing a more efficient initialization of our Java application.
If you would like to see the sample code you can find it here https://github.com/luanbrdev/crac-gradle-example
If this content helped you in any way, let me know in the comments or if something was not clear, let me know as well. See you guys!
Posted on February 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.