avatar

Andres Jaimes

Compiling Java and JavaFX applications into native binaries

By Andres Jaimes

- 7 minutes read - 1479 words

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.

  1. 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
  1. Unzip the archive:
 tar -xzf graalvm-jdk-<version>_macos-<architecture>.tar.gz
  1. 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.

JavaFX native application compiled with Gluon and GraalVM using Java 17.png

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