We are excited to announce that Cloud Java Client Libraries now have built-in support for Native Image compilation!

 

Native Image technology enables you to compile your Java applications ahead-of-time and into a standalone executable. This results in several performance benefits, such as fast cold startup times and less upfront memory usage (as it doesn’t require a JVM).

 

However, Native Image compilation isn’t always compatible with some forms of Java code (resource loading, reflection) and requires extra configuration. With this launch, Cloud Client Libraries now come with the configuration that the libraries need for native image compilation, allowing users to compile their applications without additional configurations. It is also important to note that with this technology, you lose the JVM’s run time optimizations, making native compilation best suited for short-lived workloads where quick startup and response time is key.

 

Performance Benefits

We conducted a performance comparison of an application built as a native image against the same application run with standard Java (17.0.3, Temurin) and noticed the following benefits.

 

 

 

The performance gap shown above is significant, especially when just comparing startup times. In this example, 87.65% of the native image start up times came in under 1 millisecond, which would make an enormous difference when aiming to optimize for cold start latency.

 

Memory Usage

Memory usage for the application compiled to a native image is also significantly smaller. We used the ps command to check the resident set size, which is the non-swapped physical memory that a task has used, and saw the following results:

 

Getting Started

This section will walk you through running the Pub/Sub Storage Sample with native image compilation.

 

To demonstrate the performance benefits unlocked by the client library support of native image compilation we’ll build a sample application that makes use of the Pub/Sub and Storage client libraries, informed by this guide. Feel free to follow along on your own machine, or with the Cloud Shell.

 

Prerequisites

 

To reproduce the application that we used to gather the performance data above, you will need:

 

Building the sample app

 

Start by generating our project with the following Maven goal:

 

mvn archetype:generate -DgroupId=com.mycompany.app
-DartifactId=native-image-client-libraries-sample
-DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
-DinteractiveMode=false

 

This will generate a new Maven project that will serve as a reasonable starting point for our sample app.

 

Start by setting the maven compiler plugin’s release version to 17 (or later) and adding the client libraries as dependencies:

 

<properties>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <!-Or latest version-->
        <version>25.4.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-pubsub</artifactId>
    </dependency>

 

Add a class called ListPubSubNotifications in the same directory as as App.java:

 

package com.mycompany.app;

import com.google.cloud.storage.Notification;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.util.List;

public class ListPubSubNotifications {

  public static void listPubSubNotifications(String bucketName) {

    Storage storage = StorageOptions.newBuilder().build().getService();
    List<Notification> notificationList = storage.listNotifications(bucketName);
    for (Notification notification : notificationList) {
      System.out.println(
          "Found notification " + notification.getTopic() + " for bucket " + bucketName);
    }
  }
}

 

This class will simply list any Pub/Sub notifications that are set up for a given Storage bucket.

Next, add another class in the same package called PublishWithErrorHandlerExample:

 

package com.mycompany.app;

import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.TopicName;

import java.io.IOException;

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class PublishWithErrorHandlerExample {

    public static void publishWithErrorHandlerExample(String projectId, String topicId)
            throws IOException, InterruptedException {
        TopicName topicName = TopicName.of(projectId, topicId);
        Publisher publisher = null;

        try {
            // Create a publisher instance with default settings bound to the topic
            publisher = Publisher.newBuilder(topicName).build();

            String message = String.valueOf(new Date().getTime());

            ByteString data = ByteString.copyFromUtf8(message);
            PubsubMessage pubsubMessage = PubsubMessage.newBuilder().setData(data).build();

            // Once published, returns a server-assigned message id (unique within the topic)
            ApiFuture<String> future = publisher.publish(pubsubMessage);

            // Add an asynchronous callback to handle success / failure
            ApiFutures.addCallback(
                    future,
                    new ApiFutureCallback<String>() {

                        @Override
                        public void onFailure(Throwable throwable) {
                            if (throwable instanceof ApiException) {
                                ApiException apiException = ((ApiException) throwable);
                                // details on the API exception
                                System.out.println(apiException.getStatusCode().getCode());
                                System.out.println(apiException.isRetryable());
                            }
                            System.out.println("Error publishing message : " + message);
                        }

                        @Override
                        public void onSuccess(String messageId) {
                            // Once published, returns server-assigned message ids (unique within the topic)
                            System.out.println("Published message ID: " + messageId);
                        }
                    },
                    MoreExecutors.directExecutor());
        } finally {
            if (publisher != null) {
                // When finished with the publisher, shutdown to free up resources.
                publisher.shutdown();
                publisher.awaitTermination(1, TimeUnit.MINUTES);
            }
        }
    }
}

 

This class will publish a timestamp message to a given topic, and handle exceptions in case of failure.

Finally, in App.java’s main function, add some logic to call the functions in both classes and keep track of startup/execution time:

 

package com.mycompany.app;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;

public class App 
{
    private static final Instant INITIALIZATION_TIME = Instant.ofEpochMilli(ManagementFactory.getRuntimeMXBean().getStartTime());

    private static final String projectId = "YOUR_PROJECT_ID";
    private static final String bucketName = "YOUR_BUCKET_NAME";
    private static final String topicId = "YOUR_TOPIC_ID";

    public static void main(String... args) throws IOException, InterruptedException {
        final Duration startupTime = Duration.between(INITIALIZATION_TIME, Instant.now());
        ListPubSubNotifications.listPubSubNotifications(bucketName);
        final Duration timeToFirstRequest = Duration.between(INITIALIZATION_TIME, Instant.now());
        PublishWithErrorHandlerExample.publishWithErrorHandlerExample(projectId, topicId);


        System.out.println("Startup time: " + startupTime.toMillis() + " ms, or " + TimeUnit.NANOSECONDS.toMicros(startupTime.toNanos()) + " microseconds");
        System.out.println("Time to finish first request: " + timeToFirstRequest.toMillis() + "ms");
        System.out.println("Shutting down. Total time elapsed: " + Duration.between(INITIALIZATION_TIME, Instant.now()).toMillis() + "ms");
    }
}

 

Now that the sample application is in place you are ready to configure the native image build.

 

Build configuration

Add the following `native-image` build profile to your pom.xml:

 

<profiles>
  <profile>
    <id>native-image</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.buildtools</groupId>
          <artifactId>native-maven-plugin</artifactId>
          <version>0.9.11</version>
          <extensions>true</extensions>
          <executions>
            <execution>
              <id>build-native</id>
              <goals>
                <goal>build</goal>
              </goals>
              <phase>package</phase>
            </execution>
          </executions>
          <configuration>
            <mainClass>com.mycompany.app.App</mainClass>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
  <profile>
    <id>regular-jar</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-assembly-plugin</artifactId>
          <executions>
            <execution>
              <phase>package</phase>
              <goals>
                <goal>single</goal>
              </goals>
              <configuration>
                <archive>
                  <manifest>
                    <mainClass>
                      com.mycompany.app.App
                    </mainClass>
                  </manifest>
                </archive>
                <descriptorRefs>
                  <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

 

This plugin and profile simplify the process of providing the native-image builder with the configuration it needs to build your application into a native image. Ensure that the `mainClass` parameter is correct for your application, and note that `buildArgs` can be used to pass options to the builder.

 

At this point, your app is ready to be built into a native image. There are, however, a few things worth keeping in mind with native-image’s ahead-of-time builds:

  • They take longer than equivalent just-in-time builds (approx. 5-10 minutes*)
  • They take quite a bit of memory (approx. 6-10GB*)

* values come from building this sample on the Cloud Shell’s e2-standard-4 machine type

 

To run the build, we will need to ensure that we have an appropriate JDK and the native-image builder. This process can be greatly simplified with the help of SDKMAN!.

Install the appropriate JDK distribution like GraalVM  with the following command:

 

#Or latest version
sdk install java 22.1.0.r17-grl

 

Then, inform sdkman to use this version in your current shell:

 

sdk use java 22.1.0.r17-grl

 

Next, install the native-image extension:

 

gu install native-image

 

Finally, run the build the native-image-client-libraries-sample project with the `native-image` profile:

 

#For the native image
mvn clean package -P native-image

#For the regular fat jar
mvn clean package -Pregular-jar

 

This build process may take a few minutes. Once the build finishes, you will have everything you need to see the performance differences in action!

The generated executable is in the “target” directory. Run the program to see it receives notifications from the Cloud Storage bucket

 

#For the native image
./target/native-image-client-libraries-sample

#For the regular jar
java -jar ./target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar

 

In this example, the native image started up 159x faster than the regular jar, and finished 19 times faster as well. The results of native image compilation can vary greatly depending on the workload, so feel free to experiment with your own applications to get the most out of your cloud resources.

 

Conclusion

This is only one example of how native image compilation is supported in Cloud Java Client Libraries. Please check out our official documentation to learn more about what libraries are supported and how you can build applications as native images. The documentation page also links to a couple of samples that you can try out.

 

 

By: Aaron Wanjala (Java Developer Advocate) and Cameron Balahan (Product Manager)
Source: Google Cloud Blog

Previous Network & Application Security In Google Cloud
Next REWE Group Accommodates Growth Spikes And Enhances Hybrid Architecture With Google Cloud