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.
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 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:
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:
A Google Cloud project (with the Pub/Sub and Storage APIs enabled)
A Pub/Sub topic
A Storage bucket
Building the sample app
Start by generating our project with the following Maven goal:
code_block[StructValue([(u’code’, u’mvn archetype:generate -DgroupId=com.mycompany.apprn-DartifactId=native-image-client-libraries-samplern-DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4rn-DinteractiveMode=false’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e1544be84d0>)])]
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:
code_block[StructValue([(u’code’, u'<properties>rn <maven.compiler.release>17</maven.compiler.release>rn </properties>rnrn <dependencyManagement>rn <dependencies>rn <dependency>rn <groupId>com.google.cloud</groupId>rn <artifactId>libraries-bom</artifactId>rn <!-Or latest version–>rn <version>25.4.0</version>rn <type>pom</type>rn <scope>import</scope>rn </dependency>rn </dependencies>rn </dependencyManagement>rnrn <dependencies>rn <dependency>rn <groupId>com.google.cloud</groupId>rn <artifactId>google-cloud-storage</artifactId>rn </dependency>rn <dependency>rn <groupId>com.google.cloud</groupId>rn <artifactId>google-cloud-pubsub</artifactId>rn </dependency>’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154e86abd0>)])]
code_block[StructValue([(u’code’, u’package com.mycompany.app;rnrnimport com.google.cloud.storage.Notification;rnimport com.google.cloud.storage.Storage;rnimport com.google.cloud.storage.StorageOptions;rnimport java.util.List;rnrnpublic class ListPubSubNotifications {rnrn public static void listPubSubNotifications(String bucketName) {rnrn Storage storage = StorageOptions.newBuilder().build().getService();rn List<Notification> notificationList = storage.listNotifications(bucketName);rn for (Notification notification : notificationList) {rn System.out.println(rn “Found notification ” + notification.getTopic() + ” for bucket ” + bucketName);rn }rn }rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154ef43a10>)])]
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:
code_block[StructValue([(u’code’, u’package com.mycompany.app;rnrnimport com.google.api.core.ApiFuture;rnimport com.google.api.core.ApiFutureCallback;rnimport com.google.api.core.ApiFutures;rnimport com.google.api.gax.rpc.ApiException;rnimport com.google.cloud.pubsub.v1.Publisher;rnimport com.google.common.util.concurrent.MoreExecutors;rnimport com.google.protobuf.ByteString;rnimport com.google.pubsub.v1.PubsubMessage;rnimport com.google.pubsub.v1.TopicName;rnrnimport java.io.IOException;rnrnimport java.util.Date;rnimport java.util.concurrent.TimeUnit;rnrnpublic class PublishWithErrorHandlerExample {rnrn public static void publishWithErrorHandlerExample(String projectId, String topicId)rn throws IOException, InterruptedException {rn TopicName topicName = TopicName.of(projectId, topicId);rn Publisher publisher = null;rnrn try {rn // Create a publisher instance with default settings bound to the topicrn publisher = Publisher.newBuilder(topicName).build();rnrn String message = String.valueOf(new Date().getTime());rnrn ByteString data = ByteString.copyFromUtf8(message);rn PubsubMessage pubsubMessage = PubsubMessage.newBuilder().setData(data).build();rnrn // Once published, returns a server-assigned message id (unique within the topic)rn ApiFuture<String> future = publisher.publish(pubsubMessage);rnrn // Add an asynchronous callback to handle success / failurern ApiFutures.addCallback(rn future,rn new ApiFutureCallback<String>() {rnrn @Overridern public void onFailure(Throwable throwable) {rn if (throwable instanceof ApiException) {rn ApiException apiException = ((ApiException) throwable);rn // details on the API exceptionrn System.out.println(apiException.getStatusCode().getCode());rn System.out.println(apiException.isRetryable());rn }rn System.out.println(“Error publishing message : ” + message);rn }rnrn @Overridern public void onSuccess(String messageId) {rn // Once published, returns server-assigned message ids (unique within the topic)rn System.out.println(“Published message ID: ” + messageId);rn }rn },rn MoreExecutors.directExecutor());rn } finally {rn if (publisher != null) {rn // When finished with the publisher, shutdown to free up resources.rn publisher.shutdown();rn publisher.awaitTermination(1, TimeUnit.MINUTES);rn }rn }rn }rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154ef43890>)])]
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:
code_block[StructValue([(u’code’, u’package com.mycompany.app;rnrnimport java.io.IOException;rnimport java.lang.management.ManagementFactory;rnimport java.time.Duration;rnimport java.time.Instant;rnimport java.util.concurrent.TimeUnit;rnrnpublic class App rn{rn private static final Instant INITIALIZATION_TIME = Instant.ofEpochMilli(ManagementFactory.getRuntimeMXBean().getStartTime());rnrn private static final String projectId = “YOUR_PROJECT_ID”;rn private static final String bucketName = “YOUR_BUCKET_NAME”;rn private static final String topicId = “YOUR_TOPIC_ID”;rnrn public static void main(String… args) throws IOException, InterruptedException {rn final Duration startupTime = Duration.between(INITIALIZATION_TIME, Instant.now());rn ListPubSubNotifications.listPubSubNotifications(bucketName);rn final Duration timeToFirstRequest = Duration.between(INITIALIZATION_TIME, Instant.now());rn PublishWithErrorHandlerExample.publishWithErrorHandlerExample(projectId, topicId);rnrnrn System.out.println(“Startup time: ” + startupTime.toMillis() + ” ms, or ” + TimeUnit.NANOSECONDS.toMicros(startupTime.toNanos()) + ” microseconds”);rn System.out.println(“Time to finish first request: ” + timeToFirstRequest.toMillis() + “ms”);rn System.out.println(“Shutting down. Total time elapsed: ” + Duration.between(INITIALIZATION_TIME, Instant.now()).toMillis() + “ms”);rn }rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e15441b4fd0>)])]
Now that the sample application is in place you are ready to configure the native image build.
Add the following `native-image` build profile to your pom.xml:
code_block[StructValue([(u’code’, u'<profiles>rn <profile>rn <id>native-image</id>rn <build>rn <plugins>rn <plugin>rn <groupId>org.graalvm.buildtools</groupId>rn <artifactId>native-maven-plugin</artifactId>rn <version>0.9.11</version>rn <extensions>true</extensions>rn <executions>rn <execution>rn <id>build-native</id>rn <goals>rn <goal>build</goal>rn </goals>rn <phase>package</phase>rn </execution>rn </executions>rn <configuration>rn <mainClass>com.mycompany.app.App</mainClass>rn </configuration>rn </plugin>rn </plugins>rn </build>rn </profile>rn <profile>rn <id>regular-jar</id>rn <build>rn <plugins>rn <plugin>rn <groupId>org.apache.maven.plugins</groupId>rn <artifactId>maven-assembly-plugin</artifactId>rn <executions>rn <execution>rn <phase>package</phase>rn <goals>rn <goal>single</goal>rn </goals>rn <configuration>rn <archive>rn <manifest>rn <mainClass>rn com.mycompany.app.Apprn </mainClass>rn </manifest>rn </archive>rn <descriptorRefs>rn <descriptorRef>jar-with-dependencies</descriptorRef>rn </descriptorRefs>rn </configuration>rn </execution>rn </executions>rn </plugin>rn </plugins>rn </build>rn </profile>rn</profiles>’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154d7d19d0>)])]
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:
code_block[StructValue([(u’code’, u’#Or latest versionrnsdk install java 22.1.0.r17-grl’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154d7d1750>)])]
Then, inform sdkman to use this version in your current shell:
code_block[StructValue([(u’code’, u’sdk use java 22.1.0.r17-grl’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154d7d1310>)])]
Next, install the native-image extension:
code_block[StructValue([(u’code’, u’gu install native-image’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154ee68ed0>)])]
Finally, run the build the native-image-client-libraries-sample project with the `native-image` profile:
code_block[StructValue([(u’code’, u’#For the native imagernmvn clean package -P native-imagernrn#For the regular fat jarrnmvn clean package -Pregular-jar’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154f16f4d0>)])]
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
code_block[StructValue([(u’code’, u’#For the native imagern./target/native-image-client-libraries-samplernrn#For the regular jarrnjava -jar ./target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e154f16f9d0>)])]
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.
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.
for the details.