GraalVM is a high performance Java Development Kit (JDK). It’s built on the same foundation as traditional OpenJDK setup, using the powerful HotSpot Virtual Machine (VM) underneath.
First off, you can run your Java programs on GraalVM just like you normally would. You don’t have to change anything in your code. All your usual tools and libraries work just fine. The only difference is that GraalVM uses a really smart compiler called the Graal Compiler, which is written in Java itself.
The Graal Compiler is a dynamic just-in-time (JIT) compiler, written in Java, that converts bytecode into machine code. Unlike traditional JIT compilers (like C1 and C2 in HotSpot), which are usually written in lower-level languages such as C++, Graal stands out by being implemented in Java itself.
But here’s where it gets really cool: GraalVM can also turn your Java programs into standalone applications that don’t need JDK to run. It compiles your code into a special kind of file called a native executable. So instead of needing Java installed on your computer, you can just run the executable like any other program. This not only makes your programs run faster, but it also makes them easier to distribute and use.
Moreover, GraalVM has a special framework called Truffle, which allows it to run not only Java but also other languages like JavaScript, Ruby, and Python. This means you can mix and match different languages within the same program, making it easier for them to work together seamlessly.
Some impressive features of Graal VM:
Quick Start-Up: GraalVM can compile your Java applications into standalone programs before running them. These programs are smaller, start up to 100 times faster, and use less memory and CPU compared to those running on a regular JVM.
Increased Security: By cutting out unused code and limiting certain dynamic Java features to the build time, GraalVM makes your applications more secure and less prone to attacks.
Supports Multiple Languages: GraalVM isn’t just for Java. It also supports JavaScript, Ruby, Python, and more, so you can run applications that use different languages all together.
Better Performance: GraalVM improves how your apps run by reducing delays, enhancing peak performance, and shortening garbage collection time.
Perfect for Cloud and Microservices: GraalVM works well with popular microservices frameworks like Spring Boot, Micronaut, Helidon, and Quarkus. It’s also compatible with major cloud platforms like Oracle Cloud, AWS, Google Cloud, and Microsoft Azure.
Two Versions of Graal VM are available:
Oracle GraalVM: This version comes with 24/7 support from Oracle and is licensed for commercial use.
GraalVM Community Edition: This open-source version is available under the GNU General Public License.We will be using Community Edition in this article.
💡 Important: In this article, we will try to give you a good understanding to get you up and running with Graal VM but Graal VM is too vast to cover everything in a single article. Each feature of GraalVM is so interesting that it could have its own article! If you find something about GraalVM that catches your eye, feel free to dive deeper and explore. And the best part? GraalVM is open-source, so you can tinker with it and learn more at your own pace.
Brief History:
Early Beginnings (2010s)
The GraalVM project was initiated by Oracle Labs in the early 2010s as a research project to explore new ways of executing Java bytecode.
The project was led by Thomas Würthinger, a researcher at Oracle Labs, who had a vision to create a high-performance, multi-language virtual machine.
GraalVM 0.1 (2014)
The first public release of GraalVM, version 0.1, was announced in 2014. This initial release focused on executing Java bytecode and introduced the concept of a “native image” - a ahead-of-time compiled representation of Java code.
GraalVM 1.0 (2017)
In 2017, GraalVM 1.0 was released, which added support for additional languages, including JavaScript, Ruby, and R. This release also introduced the GraalVM compiler, which enabled ahead-of-time compilation of Java code.
Acquisition by Oracle (2018)
In 2018, Oracle acquired the GraalVM project and its team, further solidifying its commitment to the technology.
GraalVM 19.0 (2019)
The release of GraalVM 19.0 in 2019 marked a significant milestone, as it introduced support for running native images as standalone executables, without the need for a JVM.
Today
GraalVM is now a widely adopted technology, used in production environments by companies such as Alibaba, and Oracle.
The project continues to evolve, with ongoing research and development focused on improving performance, security, and language support.
Benefits it provides
GraalVM offers multiple ways to run your Java applications, whether you prefer the traditional JVM method,or need faster startup and lower memory usage with native executables, or want to incorporate multiple programming languages into your project.
Traditional Java Virtual Machine (JVM) Mode
GraalVM can run your Java applications just like a regular Java Virtual Machine (JVM). This means you can compile your Java code into bytecode and run it on the JVM, benefiting from features like Just-In-Time (JIT) compilation, which optimizes your code while it runs.
Native Image Mode
GraalVM can also compile your Java applications ahead of time into native executables. This is called an “Ahead-Of-Time (AOT)” compilation. These native executables start up faster and use less memory because they don’t need the JVM to run. This is great for creating lightweight and fast applications, especially useful for cloud services or applications with high-performance needs.
💡 Ahead of Time (AOT) compilation involves a tradeoff: it requires additional setup during the build process and offers less flexibility during runtime.
Native Image:
💡 GraalVM introduces a feature called native image, which is a new concept in the Java ecosystem. It allows you to compile your Java application ahead of time into a standalone executable file.
A native image is a special version of your Java application that is compiled before you run it, rather than while it’s running. This process is called Ahead-Of-Time (AOT) compilation.
How Native Image Works
The process of creating a native executable begins with the Native Image builder, also known as native-image. This tool takes your application’s classes and metadata and transforms them into a binary file suitable for a specific operating system and architecture.
Here’s how it works:
Static Analysis: First, the native-image tool examines your code to determine which classes and methods your application will actually use when it runs.
Compilation: After analyzing your code, the tool compiles the identified classes, methods, and resources into a binary format. Instead of just compiling your Java code into bytecode (which runs on the JVM), GraalVM compiles your application into a native executable. This executable can run directly on your operating system, just like any other native program.
This entire process, known as “build time,” is separate from the usual compilation of Java source code into bytecode. It ensures that the resulting native executable is finely tuned for performance and optimized for the target environment.
Performance Benefits:
Fast Startup: Native images start up much faster than traditional Java applications because they don’t need to initialize and load the JVM.
Low Memory Usage: These native images use less memory because they don’t need the Just-In-Time (JIT) compilation process that the JVM uses. Without the need to compile code at runtime, there’s less overhead and lower memory consumption.
No Warmup: Native images are ready to perform immediately upon startup, unlike traditional JVM applications that require a warmup period to reach peak performance.
Reduced Attack Surface: By excluding unused code and limiting dynamic features to build time, native images are more secure and less vulnerable to attacks.
Compact Packaging: Native images are smaller in size, making them easier to distribute and deploy.
Lower Compute Costs: Due to their efficient use of resources and faster startup times, native images can lower compute costs, especially in cloud environments where resources are billed based on usage.
Building a Native Executable
The native-image tool transforms Java bytecode into a native executable. You can create this executable from various sources:
Class File: You can generate a native executable directly from a compiled Java class file.
JAR File: The tool can also take a JAR file, which bundles multiple class files, as input to create a native executable.
Module: For applications using Java 9 or higher, you can build a native executable from a module, allowing you to leverage the modular system introduced in Java 9.
By converting these Java bytecode formats into native executables, the native-image tool enables your applications to run directly on your operating system, bypassing the need for the JVM and thereby enhancing performance and reducing startup times.
Native Image Build Process:
What happens(In Brief):
Input Gathering: The process begins by collecting all the classes from your application, its libraries, and the Java Development Kit (JDK).
Points-to Analysis: This step involves a technique called points-to analysis. It’s like a deep dive into the code to understand how data flows and which pointers (references to memory locations) might point to which objects. This helps ensure memory safety and optimize the memory layout.
Iterative Analysis: The points-to analysis isn’t a one-time thing. It keeps running in a loop, refining its understanding of the code until no new information is discovered. This steady state is known as reaching a fixed point.
Creating the Image Heap: Using the information from the points-to analysis, an image heap is created. Think of this as a snapshot of your application’s memory at a specific point, ready to be used for further processing.
Ahead-of-Time (AOT) Compilation: Next, the image heap is used for ahead-of-time compilation. This means that the code is compiled into a native executable file, ready to run directly on the operating system without needing a Java Virtual Machine (JVM).
Text Section of the Executable: The compiled code is placed in the text section of the executable file. This section is read-only and contains all the instructions the CPU will execute.
Including Initializations: Any initializations that your application needs are also included in the executable. This ensures that when you run the executable, it starts up correctly with all necessary setups.
Substrate VM: The Substrate VM, a lightweight virtual machine tailored for running these native executables, loads the image heap. This VM provides the necessary environment for your application to run efficiently.
Heap Snapshotting: The image heap is then snapshotted, meaning it’s saved to disk. This snapshot allows for quick restarts because instead of rebuilding the heap from scratch, it can be reloaded from this saved state.
Writing the Image Heap: Finally, the image heap is written to the data section of the native executable file. The data section is a read-write memory area containing all the necessary data for the application to function.
What you get:
A native executable file that can be run directly on the target platform, without the need for a JVM or dynamic compilation.
💡 Heap Snapshotting is another big topic which we will try to cover in detail in another article.
Chapter 1: Comparing Java Code Performance: Open JDK, GraalVM JDK(Own JIT Compiler), and Native Image
We have written a Java program to test its performance, focusing on peak execution time. The program will be run in three different environments:
Open JDK - The standard Java Development Kit provided by Oracle or Amazon Corretto.
Graal VM Native Image - Compiling the Java code ahead of time into a native executable using GraalVM.
By comparing the execution times in these environments, we will see how each setup affects the performance of the Java program.
Main.java
TaskRunner.java
SimpleText.java
Explanation for the above code:
This code measures how long it takes to perform a bunch of simple tasks.
In the Main part, it starts counting time, does the tasks, then stops counting time. It shows how long it took to do the tasks.
The TaskRunner part does the actual tasks. It gets told how many tasks to do. For each task, it does a simple calculation and adds up the results.
Each SimpleTask is a single thing to do. It just does a simple math problem.
Basically, this code tests how quickly it can do simple tasks and gives us the time it took as a performance measure.
Chapter 1.1: Checking the performance with Amazon Corretto Open JDK
Output using Windows PowerShell:
Output:
It tooks 25 milliseconds to do the computation with Amazon Corretto OpenJDK.
Chapter 1.2: Checking the performance with GraalVM JDK with High Performance JIT Compiler:
Output using Windows PowerShell:
Output:
It took 25 milliseconds to complete the computation using Amazon Corretto OpenJDK. Where as GraalVM JDK completed the same computation in 22 milliseconds, which is at peak.
Chapter 1.3: Checking the performance using GraalVM Native Image(AOT)→Building a native executable from a compiled class file
Before building and running our native executable file using GraalVM, we need to set up some essential software.
Setup on Windows Platforms
Configuring GraalVM on a machine to utilize AOT native image capabilities can be quite complex. Since, i am using windows for this writeup, therefore, I have made an effort to simplify this process for Windows users:
1. Prerequisites for Native Image on Windows
To use native image on windows, one must have visual studio downloaded in their machine with Microsoft Visual C++(MSVC). On Windows, Native Image requires Visual Studio and Microsoft Visual C++(MSVC). Use Visual Studio 2022 version 17.6.0 or later.
2. Install Visual Studio Build Tools and Windows SDK
Download the Visual Studio Build Tools 2022 or later (C development environment) from visualstudio.microsoft.com
Start the installation by opening the file you downloaded, and then click Continue:
Select the Desktop development with C++ checkbox in the main window. Once you select this, a window on the right side will appear under Installation Details, make sure that the two requirements, Windows 11 SDK and MSVC (…) C++ x64/x86 build tools, are selected. Continue by clicking Install.
💡 After installing the necessary tools, you should now have the capability to compile with GraalVM Native Image.
3. Checking Existing Visual Studio Installations
If Visual Studio is already installed on your system, follow these steps to check that the correct components are installed.
Open the Visual Studio Installer from search menu in your computer:
Under the Installed tab, click Modify and choose Individual Components:
Then scroll to the bottom and confirm that the Windows 11 SDK and Visual Studio SDK checkboxes are selected. Now you can start using Native Image.
Now, once Visual Studio is setup in your machine,we will check the performance with native image also and compare it with graal vm open jdk and amazon corretto open jdk.
But What’s the purpose behind installing Visual Studio for our GraalVM Native Image setup?
One of the cool features of Graal VM is Native Image, which compiles your Java code into a standalone executable binary. This means you can run your Java apps without needing a full Java runtime environment.
Now, why do we need Visual Studio for this?
Well, on Windows, Native Image relies on some components from Visual Studio. Here’s why:
Native Image and Windows:
When you create a native image using GraalVM on Windows, it needs a little help from Visual Studio.
1. Compilation Process
Native Image: GraalVM’s Native Image compiles Java code into a native executable. This process involves translating Java bytecode into machine code that the operating system can run directly.
C/C++ Toolchain: On Windows, creating native executables from Java code using Native Image involves generating and linking native machine code. This process requires tools typically used in C/C++ development, such as a compiler and linker.
2. Role of Visual Studio and MSVC
Visual Studio: Visual Studio is an integrated development environment (IDE) that includes a suite of development tools. These tools include the Microsoft Visual C++ (MSVC) compiler and linker.
MSVC: MSVC is a crucial component provided by Visual Studio. It includes:
C/C++ Compiler: Converts source code into machine code.
Linker: Combines object files into an executable binary.
Compatibility: GraalVM Native Image requires a specific version of MSVC to ensure compatibility and proper functionality. Visual Studio 2022 version 17.6.0 or later provides the necessary MSVC tools.
3. Why Not Just Any C/C++ Compiler?
Integration and Support: MSVC is well-integrated with the Windows operating system and provides robust support for Windows-specific features and optimizations.
Dependencies: The Native Image toolchain for Windows is designed and tested to work seamlessly with MSVC. Using other compilers might result in compatibility issues or lack of necessary support for certain features.
Libraries and Headers: MSVC includes libraries and headers that are essential for building native executables. These are standard in Windows development and ensure that the generated binaries work correctly with Windows APIs.
These tools help GraalVM work its magic and create that efficient binary.
How to build native image of our code:
In our code, we begin by compiling the Main.java file to produce the essential Main.class file. This file contains the bytecode representation of our Java code. Next, we use the power of GraalVM OpenJDK to transform this Main.class file into a native executable i.e. main.exe file. This is achieved with a straightforward command: native-image Main</span>.
💡 Note: run the above command inside the folder containing these files.The .exe files generated by GraalVM Native Image are platform-specific because they are optimized to run efficiently on a particular operating system, CPU architecture, and runtime environment. This ensures maximum performance, compatibility, and seamless integration with the target platform.
Once this conversion process is completed, we are left with a platform-specific .exe file ready to be executed. We launch our newly created executable using the ./main command in the terminal. Following this command, the program springs to life, swiftly executing our code.
In our case, the execution time clocks in at a remarkable 16 milliseconds, demonstrating the efficiency of our native executable.”
Windows PowerShell:
💡 Observation:
From the above results,One can observe that using the AOT-compiled native image takes 16 ms, while using the GraalVM OpenJDK takes 40 seconds, which on peak performance could be reduced to 22 seconds. This represents a significant performance difference.
Some might think the gap isn’t a big deal, but it really matters, especially in big projects. Even small differences in how long something takes to run can add up and cause significant delays, especially in large-scale projects or when you need things to happen quickly.
This highlights why it’s important to think carefully about performance, like whether to use AOT compilation or stick with traditional JVM execution, especially when speed and efficiency are important.
Chapter 1.4: Considerations
When the JDK Might Be a Better Choice Than a Native Image
High Traffic Websites: For websites with a lot of visitors, the JDK’s Just-In-Time (JIT) compilation can optimize performance over time, making it more efficient for handling large amounts of traffic.
Heavy Memory and CPU Usage: Applications that need a lot of memory and CPU power can benefit from the JDK’s advanced garbage collection and performance tuning capabilities.
Frequent Deployments: If you’re updating and deploying your application often, the JDK allows for faster turnaround times since you don’t need to recompile the entire application into a native image each time.
Large Monolithic Applications: Big, complex applications that aren’t broken down into microservices may perform better with the JDK because it can optimize and manage the resources needed for such extensive codebases more effectively.
Java programs typically run faster on the GraalVM JDK compared to the native Java JDK because GraalVM includes advanced Just-In-Time (JIT) compilation techniques and optimizations. However, there are specific scenarios where the native Java JDK might outperform GraalVM, especially in cases where GraalVM’s advanced optimizations do not provide a significant benefit or even introduce overhead.
The native image typically has faster startup times and lower memory usage but might sometimes be slower for long-running, computationally intensive tasks due to the lack of JIT optimizations.
Chapter 2: Building a Native Executable from a JAR File
Introduction
In this chapter, we’ll explore how to leverage GraalVM to build a native executable from a JAR file, specifically focusing on developing a Spring Boot web application.
GraalVM works well with popular microservices frameworks like Spring Boot, Micronaut, Helidon, and Quarkus.
Why GraalVM with Spring Boot?
Spring Boot is a widely-used framework for building web applications and microservices due to its ease of use and extensive ecosystem. Integrating Spring Boot with GraalVM can unlock benefits such as faster startup times, reduced memory consumption, and improved overall performance.
Goal:
We’ll create a basic Spring Boot web application containing just one REST endpoint. Then, we’ll compare how long it takes for the application to start up using the traditional java -jar</span> command and with a native executable.
To begin, go to https://start.spring.io/ and generate a Maven Spring Boot project with the following dependencies:
GraalVM Native Support→The “GraalVM Native Support” dependency enables the compilation of Java applications into native executables using GraalVM, offering improved performance and reduced startup times.
Spring Web
Our code should look something like this:
HomeController.java
Explanation for the above code: This code sets up a simple web server using Spring Boot. When you visit the “/hello” URL in a web browser, it responds with the message “Hello Native Image from SpringBoot Jar”.
💡 Note: I already have Maven installed on my system, and it’s essential to use the GraalVM JDK to build the native executable from the Spring Boot jar file.
💡 Note: To make a native executable, we first need to create a jar file for our Spring Boot app. We do this by running the command mvn clean package</span>. This command compiles and packages our app into a single file that contains all the necessary code and resources.
Creating a jar file:
Windows PowerShell:
Building our native executable:
This command mvn -Pnative native:compile -DskipTests</span> instructs Maven to:
Build a native executable using the GraalVM compiler (native:compile</span>) from the Spring Boot project.
The Pnative</span> flag activates the Maven profile configured for native compilation.
The DskipTests</span> flag skips running any tests during the compilation process.
Windows PowerShell:
It took 4 minutes 46 seconds to build the native executable using jar file. This was the trade off i was previously talking i.e. build time.This build time can vary depending on the hardware specifications of the system, highlighting the trade-off between build time and system performance. Because my laptop is a bit older, the build process took longer. Normally, it should take around 2 minutes, which is sufficient for the build.
After running the command, a .exe file i.e. native executable is generated as shown in the below image.
Now let’s see, how fast our Spring Boot application starts up by running the jar file with the usual java -jar fileName</span> command.
Windows PowerShell:
Our SpringBoot application started in 2.747 seconds.
Executing our native exectuable:
Now, we’ll run our native executable and check how quickly our application starts up.
PS D:\GraalVMSpringDemo> ./target/GraalVMSpringDemo
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.0)
2024-05-30T01:47:44.203+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] c.unlogged.GraalVmSpring
DemoApplication : Starting AOT-processed GraalVmSpringDemoApplication using Java 22.0.1 with PID 4220 (D:\GraalVMSpringDemo\target\GraalVMSpringDemo.exe started by Gaurav in D:\GraalVMSpringDemo)
2024-05-30T01:47:44.204+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] c.unlogged.GraalVmSpringDemoApplication : No active profile set, falling back to 1 default profile: "default"
2024-05-30T01:47:44.230+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-05-30T01:47:44.232+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-05-30T01:47:44.232+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.24]
2024-05-30T01:47:44.241+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-05-30T01:47:44.241+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 37 ms
2024-05-30T01:47:44.265+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2024-05-30T01:47:44.267+05:30 INFO 4220 --- [GraalVMSpringDemo] [ main] c.unlogged.GraalVmSpringDemoApplication : Started GraalVmSpringDemoApplication in 0.078 seconds (process running for 0.086)
Conclusion:
When we ran the native image of the Spring Boot jar file, it started up in just 0.078 seconds. However, if we run the jar file directly using java -jar</span> from the command prompt, it takes 2.747 seconds. This time difference tends to get bigger as our application gets larger. But it’s worth noting that building the native image takes longer, so there’s a trade-off to consider.
Chapter 3: Some Additional Stuff
We are not going into detail of these.Feel free to explore if you find them intriguing.
Additional Feature that comes with using GraalVM
Polyglot Capabilities:
GraalVM supports running code written in other programming languages along with Java. This means you can write parts of your application in languages like JavaScript, Python, Ruby, or R, and run them together seamlessly. This can be very handy if you want to use different languages for different tasks within the same project.
Additional Ongoing Graal Projects:
💡 Please understand that it’s not possible to cover everything in just one article, so there will be more articles on this topic coming your way soon.