I was able to drastically improve the performance of my Spring Boot REST API by generating a Native Image with the GraalVM toolkit. The “drastically” adjective translates to numbers as:
Startup Time | Memory Usage | Most CPU Usage | |
---|---|---|---|
With GraalVM JVM | 3,310 ms | 280.9 MB | 15% |
Native Image | 0,157 ms | 147.2 MB | 0.13% |
Comparison | 95% less | 50% less | 98% less |
If you like to skip to the How-To, click here.
I developed a backend REST API to act as the BFF for my portfolio-website
. All it did was make authenticated requests to GitHub’s API and cache those responses. On my professional job, I got used to deploying applications without worrying too much about resources, so you can image my surprise when this tiny API ran out of memory and went down.
The first thing that came to my mind was that Fly.io’s free plan gave an unrealistically small memory size. But when actually looking at it, the memory limit was of 228 MB. For an API that did so little, 228 MB should have been more than enough. Then I realized why some people trash talk Java and the JVM… My app was using up to 280 MB for that simple flow. So I had to either increase the machine’s memory or reduce its usage.
Fly.io offers more memory if you pay 5 dollars, but I did not want to give up that easily. So I tried a little more and remembered about GraalVM. I knew it was great to speed up the application cold-start and overall performance. But I did not expect these improvements.
As shown with the table on the beginning of this article, the memory usage reduced ~ 50%. Besides that, the cold-start and average CPU usage also improved a lot. And with the memory usage reduction, I can now rest assured that my API will not run out of memory.
This is the application running with the JVM. The last column represents the memory reserved.
This is the application running with the GraalVM.
GraalVM is a set of tools which include the Native Image builder. The Native Image builder reads the bytecode produced by the JDK compiler and performs ahead-of-time compiling. The difference between the two compilations is that the JDK compiler generates bytecode, whereas the Native Image builder generates binaries. The bytecode is interpreted by the JVM and can run anywhere a JVM can. The binaries, on the other hand, run directly on the OS and are specific to an OS and processor architecture.
Visual representation of actor in the native image building processes.
The build process requires a lot of RAM. My environment has an Intel Core i5-1035 with 8 GiB of RAM and when I had less than 4 GiB free, I got an OutOfMemoryException
from the native image builder. So be sure to have at least 4 GiB of RAM available before building your image.
If you plan on deploying your application with a Docker image, and have a Docker daemon running on your environment, the easiest way to build your image is to use Buildpacks. The Spring Boot plugin sets up everything for you. Just jump to 3.2 Setting up Gradle and continue from there.