Compiling Java and JavaFX applications into native binaries
This article will walk you through the process of compiling Java and JavaFX applications into native binaries using GraalVM and Gluon. This version is initially focused on MacOS, but will be updated for Windows.
Installing GraalVM
GraalVM is a software package that includes a Java SDK and tools for converting Java programs into native applications. I recommend you to use this SDK during your development. Let’s start by downloading GraalVM. The available Java version as of the publication of this page is 22. There are versions for both Intel and Apple processors.
- The first step after downloading it is to remove the quarantine attribute added by OSX:
sudo xattr -r -d com.apple.quarantine graalvm-jdk-<version>_macos-<architecture>.tar.gz
- Unzip the archive:
tar -xzf graalvm-jdk-<version>_macos-<architecture>.tar.gz
- Move the unzipped directory to
/Library/Java/JavaVirtualMachines
. This is an important step. I tried to run it from different directories, but OSX complained that the SDK was damaged. I was able to fix it only when I moved it to the aforementioned directory.
sudo mv graalvm-jdk-<version>_macos-<architecture> /Library/Java/JavaVirtualMachines
Compiling and running our first native Java program
Start by creating the following HelloWorld.java
file:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Now, create a compile.sh
shell script with the following contents:
#!/bin/sh
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-jdk-22.0.1+8.1/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH
javac HelloWorld.java
native-image HelloWorld
Setting JAVA_HOME
is very important for this process, because this is the way you select which SDK you’re going to use. This variable has to point to the directory you used in the previous section.
After setting PATH
, and calling the java compiler (javac), you’ll notice the call to native-image
. This program is part of GraalVM and is the one that makes the magic of converting the resulting HelloWorld.class
file into a native program. Run your script:
./compile.sh
The compilation and linking takes some seconds, but once it’s done, you’ll find a helloworld
file in the directory. Go ahead and run it:
./helloworld
Hello World!
Congratulations! You have a native program.
For reference, I’m including native-image
's output here. You should expect to see something similar.
java version "22.0.1" 2024-04-16
Java(TM) SE Runtime Environment Oracle GraalVM 22.0.1+8.1 (build 22.0.1+8-jvmci-b01)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 22.0.1+8.1 (build 22.0.1+8-jvmci-b01, mixed mode, sharing)
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
[1/8] Initializing... (3.0s @ 0.10GB)
Java version: 22.0.1+8, vendor version: Oracle GraalVM 22.0.1+8.1
Graal compiler: optimization level: 2, target machine: armv8-a, PGO: off
C compiler: cc (apple, arm64, 15.0.0)
Garbage collector: Serial GC (max heap size: 80% of RAM)
1 user-specific feature(s):
- com.oracle.svm.thirdparty.gson.GsonFeature
------------------------------------------------------------------------------------------------------------------------
Build resources:
- 12.09GB of memory (75.6% of 16.00GB system memory, determined at start)
- 8 thread(s) (100.0% of 8 available processor(s), determined at start)
[2/8] Performing analysis... [*****] (3.5s @ 0.20GB)
2,039 reachable types (59.7% of 3,414 total)
1,842 reachable fields (38.8% of 4,749 total)
8,587 reachable methods (35.4% of 24,275 total)
735 types, 25 fields, and 335 methods registered for reflection
49 types, 33 fields, and 48 methods registered for JNI access
4 native libraries: -framework Foundation, dl, pthread, z
[3/8] Building universe... (0.6s @ 0.23GB)
[4/8] Parsing methods... [*] (0.4s @ 0.23GB)
[5/8] Inlining methods... [***] (0.5s @ 0.25GB)
[6/8] Compiling methods... [***] (7.3s @ 0.31GB)
[7/8] Laying out methods... [*] (0.5s @ 0.27GB)
[8/8] Creating image... [*] (0.7s @ 0.28GB)
2.62MB (44.07%) for code area: 3,866 compilation units
3.13MB (52.63%) for image heap: 51,661 objects and 43 resources
200.73kB ( 3.30%) for other data
5.94MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area: Top 10 object types in image heap:
1.25MB java.base 691.13kB byte[] for code metadata
1.14MB svm.jar (Native Image) 689.84kB byte[] for java.lang.String
80.59kB com.oracle.svm.svm_enterprise 361.95kB java.lang.String
25.31kB org.graalvm.nativeimage.base 329.34kB java.lang.Class
22.78kB org.graalvm.collections 160.75kB heap alignment
22.27kB jdk.proxy3 142.63kB java.util.HashMap$Node
19.61kB jdk.proxy1 114.52kB char[]
14.72kB jdk.graal.compiler 82.98kB java.lang.Object[]
14.23kB jdk.internal.vm.ci 79.65kB com.oracle.svm.core.hub.DynamicHubCompanion
8.26kB jdk.proxy2 79.46kB byte[] for reflection metadata
456.00B for 1 more packages 467.75kB for 530 more object types
Use '-H:+BuildReport' to create a report with more details.
------------------------------------------------------------------------------------------------------------------------
Security report:
- Binary includes Java deserialization.
- Use '--enable-sbom' to embed a Software Bill of Materials (SBOM) in the binary.
------------------------------------------------------------------------------------------------------------------------
Recommendations:
PGO: Use Profile-Guided Optimizations ('--pgo') for improved throughput.
HEAP: Set max heap for improved and more predictable memory usage.
CPU: Enable more CPU features with '-march=native' for improved performance.
QBM: Use the quick build mode ('-Ob') to speed up builds during development.
------------------------------------------------------------------------------------------------------------------------
1.0s (5.8% of total time) in 216 GCs | Peak RSS: 0.76GB | CPU load: 5.50
------------------------------------------------------------------------------------------------------------------------
Build artifacts:
/Users/andres/devel/graalvm/01/helloworld (executable)
========================================================================================================================
Finished generating 'helloworld' in 16.9s.
A JavaFX example
Since most programs extend beyond simple Hello-World applications, we’ll now compile a JavaFX application. The process is similar, but with one major difference: we’ll use a specialized GraalVM distribution called Gluon.
Gluon is similar to GraalVM but includes essential libraries for compiling and linking JavaFX applications. It’s important not to skip this step if you’re working with JavaFX.
Also, you need a working version of XCode and its developer tools. If you’re a MacOS developer, I bet you’ve used this command before:
xcode-select --install
Once Xcode is installed and running, let’s move forward with GraalVM and Gluon.
Installing GraalVM + Gluon
As of this article’s publication, the latest version of Gluon works with Java 17. Go ahead and download it from the following link:
https://github.com/gluonhq/graal/releases/tag/gluon-22.1.0.1-Final
The installation process is very similar to the process we saw in the first section of this article. We start by removing the quarantine attribute from the downloaded file:
sudo xattr -r -d com.apple.quarantine graalvm-svm-<java-version>-<architecture>-gluon-<version>-Final.tar.gz
Unzip the file:
tar -xzf graalvm-svm-<java-version>-<architecture>-gluon-<version>-Final.tar.gz
And move it to the /Library/Java/JavaVirtualMachines
. At this point there’s a chance you have multiple virtual machine directories in this location, which is fine. As before, I tried to use this SDK from different directories, but OSX kept complaining that I had a damaged program. The problem was fixed once I moved it to the aforementioned directory.
sudo mv graalvm-svm-<java-version>-<architecture>-gluon-<version>-Final /Library/Java/JavaVirtualMachines
Let’s download maven’s 3.8 version from the Apache website, otherwise you may end up with the following error:
Error: Maven version 3.9.6 is not currently supported by the GluonFX Maven Plugin.
So go ahead and download the latest 3.8.x version:
https://maven.apache.org/download.cgi
Unzip it and move it to a directory where you can easily access it. I placed it into a new ~/java/mvn
directory.
mkdir -p ~/java/mvn
tar -xzf apache-maven-3.8.8-bin.tar.gz
mv apache-maven-3.8.8 ~/java/mvn
Finally, clone Gluon’s JavaFX sample projects. They can be cloned from the following link:
https://github.com/gluonhq/gluon-samples/tree/master
Let’s cd
into the HelloFX
directory and create the following compile.sh
program:
#!/bin/sh
# mvn uses the Java version set via JAVA_HOME
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-svm-<java-version>-<architecture>-gluon-<version>-Final/Contents/Home
export GRAALVM_HOME=$JAVA_HOME
# we need maven 3.8
~/java/mvn/apache-maven-3.8.8/bin/mvn gluonfx:compile
~/java/mvn/apache-maven-3.8.8/bin/mvn gluonfx:link
It’s important that you set the JAVA_HOME
directory to point to the GraalVM + Gluon SDK version, otherwise there’s a chance the program does not pick the Java version in the Gluon distribution.
Additionally, we’ve introduced a variable called GRAALVM_HOME
that also points to the same directory. Finally, by adding the full path to the mvn
program, we’re making sure we use the maven version we downloaded in the previous step. Maven will use the Java version pointed by the JAVA_HOME
variable.
Let’s run it.
./compile.sh
If everything goes right, you’ll get after some seconds a native JavaFX application. The program can be found at target/gluonfx/<architecture>/
, for example: target/gluonfx/aarch64-darwin/
. Go ahead and run it:
cd target/gluonfx/aarch64-darwin/
./HelloFX
Give yourself a pat on the back.
Conclusion
In this article, we have described the process to install the required software to compile Java and JavaFX applications into native binaries, we have compiled a couple of applications into native binaries, and we have discussed the issues that I faced to make it work.
The process is straightforward if you follow the process. But the fun does not end here. Check the Gluon references at the bottom of this page. You’ll find that you can use the special package
parameter to create full MacOS applications and application installers for OSX and Windows.
Also, this workflow is a good starting point to go beyond and experiment with iOS and Android applications.
I hope you have as much fun as I did working with it.
Troubleshooting
Other than the issues already discussed, I was presented with the following error while testing a JavaFX application:
com.sun.glass.ui.mac.MacApplication lambda$waitForReactivation$6 WARNING: Timeout while waiting for app reactivation
This is a bug with some MacOS versions which has been solved. Make sure you use any of the JavaFX versions where this bug has been fixed.
User pfurbacher in Stackoverflow posted:
The issue has been fixed and backported. Here are the versions in which it has been fixed: jfx21.0.2, jfx21.0.1, jfx17.0.9, 8u401.
References
- Apache Maven. The Apache Software Foundation. https://maven.apache.org/index.html
- Gluon MacOS Platform. GluonHQ. https://docs.gluonhq.com/#platforms_macos
- Gluon Downloads. GluonHQ. https://github.com/gluonhq/graal/releases/tag/gluon-22.1.0.1-Final
- Gluon Samples. GluonHQ. https://github.com/gluonhq/gluon-samples/tree/master
- Installation on macOS Platforms. GraalVM. https://www.graalvm.org/latest/docs/getting-started/macos/
- Maven plugin for GraalVM Native Image building. GraalVM. https://graalvm.github.io/native-build-tools/latest/maven-plugin.html
- JavaFX window takes a while to load and getting an error. StackOverflow. https://stackoverflow.com/a/77272610/2079513