Is Java Going Native?
Discover more about how the introduction of Java Native Images signifies major progress in tackling modern app challenges, such as fast startup times and efficient resource usage.
Alin Bobeica (Java Software Engineer) wrote this article, which was previously featured in the May 2024 issue of Today Software Magazine.
![Is Java Going Native?](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2FBCBdN1gsu8f1jRx6IpJnL%2Fd39b41abc8169cd016b79f307f1aea99%2FCareers_Website_TSM_articles_visuals_-_rounded_corners__711___380_px___10_.png%3Fh%3D639%26w%3D1136%26fit%3Dthumb&w=3840&q=75)
The way we build applications has evolved since 1995, when Java was first introduced. We have progressed from using monoliths to microservices and, in recent years, to serverless deployments.
However, these changes also bring new challenges, such as fast startup time and instant peak performance, which are now essential for software applications. To address these challenges, Java has introduced Native Images.
What are Native Images?
Native Image is a technology that uses ahead-of-time compilation to transform Java code into standalone executables called native images.
This results in faster startup times, more efficient resource usage, improved security, and more compact packaging.
Ahead-Of-Time Compilation (AOT)
In traditional Java applications, the compilation method used is a just-in-time compilation (JIT). JVM is responsible for executing Java bytecode and compiling frequently used code into native code based on profiling information collected during runtime.
The benefit of this compilation is that it enables the JVM to deliver optimised code for improving peak performance and the so-called platform-independent application. But these advantages also come with certain disadvantages, JIT puts much stress on JVM during the runtime, thus resulting in slower startup times and cold start.
Additionally, JIT requires a JVM for executing the code, resulting in higher memory usage and larger memory footprint. AOT aims to improve these drawbacks.
Ahead-of-time Compilation refers to the technique of translating a high-level language, in our case Java, into a low-level language before the application is executed usually at build time. The output of the AOT will be a platform-specific binary that does not need further compilation.
AOT Compilation is used in Java applications to improve the overall performance but most importantly, to reduce start-up time. The most known AOT compiler in Java is GraalVM.
What is GraalVM?
GraalVM started as a research project at Oracle in their research & development branch called Oracle Labs. Oracle Labs is responsible for investigating programming languages and virtual machines, machine learning and security, graph processing, and other areas.
GraalVM's main product is the Graal compiler, which is a highly-optimising compiler, created from scratch. Surprisingly this compiler is mostly written in Java and in many scenarios, thanks to the many optimisations, it's performing better than the C2 compiler. This demonstrates once again how powerful Java is.
How does GraalVM work?
GraalVM takes the Java bytecode as an input and generates a native executable. This executable is platform specific. How is GraalVM doing this? Well, it's performing something called static analysis, where the compiler looks over the code that your application is using and only includes that code in the native output.
To this end, the compiler uses these three techniques:
Points-to analysis: Points-to analysis is a compile-time technique that helps identify relationships between pointer variables and the memory locations that they point to during programme execution. GraalVM uses this technique to determine which Java classes, methods and fields are reachable at runtime, and include only those in the output executable. The analysis starts with the main method as an entry point and iteratively processes all transitively reachable code until a fixed point is reached. This analysis is applied to application code and external libraries and JDK classes. Every code that is needed is packed in the native executable.
Initialisations at build time: Class initialisation at runtime adds a lot of overhead when starting an application. For example, a simple “Hello, World!” application requires more than 300 classes to be initialised. To reduce this overhead, GraalVM supports class initialisation at build time, making runtime initialisation and checks unnecessary. All the static state from initialized classes is stored in the executable. Access to a class’s static fields that were initialised at build time is transparent to the application and works as if the class was initialised at runtime. However, this also comes with some constraints:
When a class is initialised, all its superclasses and superinterfaces with default methods must also be initialised.
No instances of classes that are initialised at runtime must be present in the executable.
Heap snapshotting: When a class is instantiated at build time, the resulting Java object is written onto the image heap that is included in the native executable.
These three steps are repeated until a fixed point is reached.
![TSM_Visuals_May_Fig1_Website.png](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2F6u2b6jl4fQNN3avkGZ2wK3%2F120a3c08bce93650a0dc19151107086b%2FTSM_Visuals_May_Fig1_Website.png&w=3840&q=75)
Created by the author: GraalVM Compilation Process
For handling runtime features, like memory management and thread scheduling, Native Images includes a slim VM called SubstrateVM. This special VM is also written in Java.
Why Use Native Images?
Native Images have several benefits. Here, we will discuss some of the main advantages of using them.
Instant* startup time: Native images have faster startup times compared to JVM-based apps. There's no need for JIT compilation or interpretation, making executables launch immediately. The pre-population of the heap also eliminates the so-called "cold start" that Java applications suffer from. This creates a more responsive user experience, especially for short-lived applications.
![TSM_Visuals_May_Fig2 (1).png](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2F4KcbZtcq86uO8OjIo5K4So%2F8d3b7127581f5f2fbb041bec98f70ec5%2FTSM_Visuals_May_Fig2__1_.png&w=3840&q=75)
Created by the author: JIT vs AOT Startup
Optimised Resource Utilisation: By compiling the Java code specifically for the target platform, Native Images can leverage platform-specific optimisations, leading to better utilisation of system resources. This optimizstion can result in improved scalability and performance for Java applications.
Reduced Memory Footprint: Since the Native Images only contain the necessary components, Native Images usually have a smaller memory footprint compared to classic JVM applications.
Minimise Vulnerability: Including only the used code in the final image, the attack surface is reduced. Additionally, by compiling the Java code ahead of time into machine code, eliminates the need for the JVM, further reducing the attack surface and mitigating the risks associated with dynamic code execution.
Native Image Example with Spring Native
Let's write a simple example using Spring Native and Maven for building native images. For this, I will use the Community Edition GraalVM which is based on OpenJDK. The Java version used for this example is 21. GraalVM also comes with a Maven plugin that helps you build and test native executables using Maven.
Spring Native is a project within the Spring Framework that provides support for building spring applications as native images.
I've written a simple CRUD application for managing productsthat exposes 4 endpoints for creating, deleting, and listing products. I will make two versions of this application, one JIT compiled that runs using JVM and the other one will be AOT compiled as a native image, and I will compare the performance of these two applications.
First, we need to add the graalVM Maven plugin to our application pom.xml, this will provide us the capability to create native images using Maven.
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Now, let's build the application using the Maven command:
./mvnw -Pnative native:compile
After the build is done, we can see in the target directory - a new standalone native executable.
Let's compare the performance of the native image application with the one that is running using JVM.
Startup time: As the Java bytecode does not need to be interpreted and compiled, the Native Image starts immediately when compared to the JVM application. Also, due to the prepopulated heap and direct compilation to native machine language, the cold start is eliminated.
![fig-3.1-1-.png](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2F5IWmiza5cUlGyZnfZj8KC2%2F6cb7b7f61c39cb9074ec762363d2efc7%2Ffig-3.1-1-.png&w=1920&q=75)
Created by the author: Native Image Startup Log
![fig-3.2-1-.png](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2F6OMb4hCvIWhqDFyPnpIth2%2Fc03c20363533f71bdd8789b9e3fb7cbf%2Ffig-3.2-1-.png&w=1920&q=75)
Created by the author: JVM Application Startup Log
In the above images we can see the startup logs of the Spring application.The one that is compiled to the Native Image took only 143 ms, compared to the JVM one, which took 2.834 seconds.
Memory efficiency: Because the Java bytecode is compiled ahead of time, the native executable doesn't require the JVM, its JIT compilation infrastructure, memory for compiled code, profile data, or bytecode caches, it only needs memory for the executable and the application data.
![fig-4-1-.png](/_next/image?url=https%3A%2F%2Fimages.ctfassets.net%2Fovgp1f3fq8f3%2FloWQ6rOjDoyqNVrOdAIsy%2Fad9e2eb4d3cfe928826a3eefa80086f9%2Ffig-4-1-.png&w=3840&q=75)
Created by the author: Native Image vs. JVM CPU and Memory Consumption
The charts reflect the CPU and memory composition of the two applications. The blue line represents the memory usage, around 390 MB for the JVM application vs. 120 MB for the native executable. The red line represents the CPU activity.
On one hand, we can see that the JVM application consumes much more CPU during the warmup period, because of all the JIT operations that need to be done. On the other hand, the native executable uses much less CPU all the heavy operations being performed ahead of time.
What’s the Catch?
Probably now you may say, this is too good to be true, well, the native images also come with some downsides and there are some things to keep in mind when working with native images in Java.
Here are some drawbacks of the Native Images:
The building time is exponentially increased:
Because all the runtime stress is moved at compilation time, due to ahead-of-time compilation, the building time is exponentially increased, and creating native images requires more resources. Also, the resulting native executable is platform-specific and will be compatible with the architecture of the system it was built on.
Limited support for reflection:
Because the Native Image performs static analysis under a closed-world assumption, dynamic features like reflection, need additional configurations for the compiler to be aware of its use at the building time. When performing static analysis, Native Image will try to detect and handle calls to the Reflection API, however, this automatic analysis is not always enough and you may need to give the compiler some hints. This type of configuration may be needed also when using Java Native Interface (JNI), Dynamic Proxy objects, and classpath resources.
Compatibility concerns:
Native images may not support all Java features or libraries out of the box, especially those that rely heavily on dynamic class loading or bytecode. Ensuring compatibility with third-party libraries and frameworks may require additional effort or modifications to the codebase. Some of the libraries/frameworks that may raise some challenges when running as native images are Hibernate, AspectJ, and Spring AOP.
Conclusion
In conclusion, the introduction of Native Images in Java marks a significant advancement in addressing modern application challenges such as fast startup times and efficient resource utilisation. By utilising ahead-of-time compilation and technologies like GraalVM, Java developers can now produce standalone executables with improved performance and reduced memory footprint.
The benefits of native images, including instant* startup time, optimised resource utilisation, and minimised vulnerability, make them a good option for various use cases, especially for short-lived applications and those requiring enhanced security measures.
However, it's essential to acknowledge the drawbacks associated with Native Images, such as increased building time, limited support for reflection, and compatibility concerns with certain Java features and libraries. Addressing these challenges may require additional effort and careful consideration during development.
Overall, while Native Images offer promising advantages, developers should assess their suitability for specific projects based on performance requirements, compatibility considerations, and resource constraints. With proper understanding and mitigation of potential challenges, Native Images can be a valuable tool in modern Java application development, enabling enhanced performance and scalability in diverse deployment environments.
References