DEV Community

Luan Barbosa Ramalho
Luan Barbosa Ramalho

Posted on

Do you want to improve the start-up and warm-up times of your Java applications? Maybe the CRaC project can help you.

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.

Image description

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;
}
Enter fullscreen mode Exit fullscreen mode

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<>());
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)