As I mentioned in this article, I was planning to move my Cloudflare DDNS solution to GraalVM native image in order to save on the memory footprint, because realistically it didn’t make much sense for such a simple piece of software. This is true of most simple software written in Java, you have to allow for the JVM memory footprint, which is > 150 MB. If the application itself eats enough memory, this becomes irrelevant, but for a small application it might be the case that 70-80% of used memory is for the JVM. The other big advantage of GraalVM native images is startup time, which is great in situations like a serverless application that needs to go from zero to one very fast, but this wasn’t the case for my application.
The good
Just to be clear, the stated objectives are not what I’m disappointed about. In fact, it achieves them very well. I can attest that the memory footprint went from 171MB to 47MB:

And the startup time went from 4.8 seconds to 0.1 seconds:


The bad
Now on to the unpleasant stuff. I identified 3 main issues while working with GraalVM native images. I will list them from least bad to worst.
First off, it seems that even for a simple application, like the one I experimented with, you have to give GraalVM hints about classes it needs to compile into the native image. In my case, I used a class hierarchy where the top object is annotated with @ConfigurationProperties
in order to map application properties to some objects. Something like this:
@ConfigurationProperties(prefix = "cloudflare")
public record CloudflareProperties(
CloudflareDnsTrace dnsTrace,
CloudflareApi api,
List<CloudflareZone> zones
) {
}
While the top-most object is known to the compiler, probably because of the @ConfigurationProperties
annotation, the child classes are not, so you will run into runtime errors related to those classes.
So I had to add something like this specifically to make GraalVM aware of them:
@RegisterReflectionForBinding({
CloudflareProperties.class,
CloudflareApi.class,
CloudflareDnsTrace.class,
CloudflareZone.class
})
The second one is that the feedback loop during development is very slow. This might not seem like a big deal at first, I was thinking that most of the development is done as a regular Java application anyway, so I will rarely build the native image itself. But then you run into issues like the one I mentioned above, that are specific to the native image itself, and for each attempt to solve it takes 5 minutes to find out if it works or not. Because that’s how long it takes to build the native image for a very small application on a decent laptop. I hope the time does not grow linearly with the size of the application’s codebase, because if it does it becomes unusable even for medium sized applications.
This will break your focus as a developer, you will not idle for 5 minutes, you will move on to something else. And then you remember to come back to it, it didn’t work or something else broke, and you have to wait another 5 minutes. So the process slows down a lot.
The third one is somehow related to the second one, but much worse. I wanted to dockerize the application, first because that’s how I wanted to use it and second to offer it as such to whoever wants to use it. And I wanted the Docker image to be multi-architecture, for both amd64
and arm64
.
So in your pipeline you will have to build 2 binaries, one for amd64
and one for arm64
. But your pipeline will run on some machine (amd64
or arm64
), unless you have runners for both architectures. That machine will likely be amd64
. The way to build the arm64
native image in the pipeline is to use Docker QEMU. In Github Actions, which is what I used, you can use docker/setup-qemu-action@v3
to set it up.
The good news is that there’s a solution. The bad news is that this emulation is unbelievably slow. I kid you not, it takes 45 minutes to build the arm64
native image in Github Actions. And of course you’re not gonna get it right on the first go, so you’ll have to try again and again.
All in all, it took me about 3 days to set up the pipeline properly. It’s true however that you only set up the pipeline once.
So there it is, the pros and cons of running your JVM application as a GraalVM native image. Weigh them for your use case and decide. My guess is that for most cases the costs are too big for the benefits, and in those situations where the benefits are worth it, it might make sense to look at something other than Java.
For my Cloudflare DDNS project, I plan to leave it using GraalVM native images. First off because I’ve already put in the effort and it’s working. And second off because it’s small enough. Even if it’s gonna evolve in the future with extra features (I kinda doubt it), it will still remain a small script calling the Cloudflare API, so I think it will be manageable even with the slow feedback loop.
Hope this helps, have fun clickity-clacking.
Leave a Reply