By Jun Liu
In the era of cloud computing, Java applications face challenges such as slow "cold start," high memory usage, and long warm-up time, which impede their adaptability to cloud deployment modes like Serverless. GraalVM addresses these issues by leveraging static compilation, packaging, and other technologies. Additionally, popular frameworks like Spring and Dubbo offer compatible Ahead-of-Time (AOT) solutions to accommodate GraalVM's usage limitations.
This article provides a comprehensive analysis of the challenges Java applications encounter in the cloud era, the solutions offered by GraalVM Native Image, and the fundamental concepts and working principles of GraalVM. Furthermore, it demonstrates the process of statically packaging a typical microservices application using the Spring 6 and Dubbo 3 frameworks.
This article is divided into the following four parts:
First, let's take a look at the application characteristics in the cloud computing era and the challenges that Java faces in the cloud era. According to various statistical agencies, Java remains one of the most popular programming languages for developers today, second only to certain scripting languages. Java enables efficient development of business applications, thanks to its rich ecosystem, which enhances productivity and operational efficiency. Consequently, numerous applications have been developed using the Java language.
However, in the era of cloud computing, Java applications encounter several deployment and operational challenges. Let's consider Serverless as an example. Serverless is an increasingly prevalent deployment model in the cloud that allows developers to focus on business logic and address resource issues through rapid elasticity. However, Java's representation in the Serverless runtime across various cloud computing vendors is relatively small, significantly lower than its prevalence in traditional application development.
The main reason for the situation is that Java applications cannot meet the key requirements of Serverless scenarios.
• First, Java has a relatively long cold start time. This is a major challenge for Serverless scenarios where fast bounce is required because the pull-up time of Java applications may be seconds or tens of seconds.
• Second, Java applications require warm-up time to achieve optimal performance. It is inappropriate to allocate a large amount of traffic to applications that have just been pulled up because there will be problems such as request timeout and excessive resource occupation, further extending the effective startup time of Java applications.
• Third, Java applications have high demands for the running environment. It often requires a lot of memory and computing resources. However, instead of being allocated to the business needs, most of them are consumed by the JVM runtime, which contradicts the goal of cost reduction and efficiency improvement in the cloud.
• Finally, Java applications produce large packages or images, affecting overall storage and retrieval efficiency.
Next, let's take a specific look at how GraalVM, a packaging and runtime technology, solves these problems faced by Java applications.
GraalVM compiles your Java applications ahead of time into standalone binaries that start instantly, provide peak performance with no warmup, and use fewer resources.
According to the official introduction, GraalVM provides AOT compilation and binary packaging capabilities for Java applications. Binary packages based on GraalVM have the following advantages: fast startup, ultra-high performance, no warm-up time, and little resource consumption. The AOT mentioned here is a technical abbreviation that occurs during compilation, namely Ahead-of-time, and I will talk about it later. In general, GraalVM can be divided into two parts.
• First, GraalVM is a complete JDK release version, which is equivalent to OpenJDK and can run applications developed in any JVM language.
• Secondly, GraalVM provides the Native Image packaging technology, which can package applications into binary packages that can run independently. This package is a self-contained application that can run separately from the JVM.
As shown in the above figure, the GraalVM compiler provides two modes: JIT and AOT.
• JIT mode: We all know that Java classes will be compiled into files in the .class format, which have become the bytecode recognized by JVM after compilation. When Java applications are running, the JIT compiler compiles some bytecode on hot paths into machine code to achieve faster execution speed.
• AOT mode: AOT directly converts bytecode into machine code during compilation, eliminating the dependency of runtime on JVM. Because JVM loading and bytecode runtime warm-up time is saved, the programs compiled and packaged by AOT have high runtime efficiency.
In general, the JIT mode enables applications to have higher limit processing capability. It can reduce the key metric, the maximum latency of requests. The AOT mode can further improve the cold start speed of applications, have smaller binary package sizes, and require less memory and other resources in the running state.
We have mentioned the concept of Native Image in GraalVM many times above. Native Image is a technology that compiles Java code and packages it into executable binary programs. The produced package only contains the code required at the runtime, including the application's code, standard dependency package, language runtime, and static code associated with the JDK library. This package no longer relies on the JVM environment for execution. However, it is tied to specific machine environments and requires separate packaging for different machine environments.
Native Image has the following features:
• Contains only a portion of the resources needed to run the JVM, so the running cost is lower
• Starts in milliseconds
• Enters the best state without warming up after starting
• Supports to be packaged as a lighter binary package, making deployment faster and more efficient
• More secure
To sum up, Native Image provides faster startup speed, less resource usage, less risk of security vulnerabilities, and a more compact binary package size. It effectively addresses prominent challenges faced by Java applications in cloud computing scenarios like Serverless.
Next, let's take a look at the basic usage of GraalVM. First, you need to install the relevant basic dependencies required by native-image, which may vary depending on the operating system environment. Next, you can use the GraalVM JDK downloader to download native-image. Once everything is installed, you can proceed to compile and package Java applications using the native-image command. The input can be class files, jar files, Java modules, etc., which will ultimately be packaged into an executable file capable of independent execution, such as the HelloWorld example. Additionally, GraalVM provides Maven and Gradle build tool plugins that facilitate the packaging process.
GraalVM, based on a concept called "closed world assumption", requires that all runtime resources and behaviors of a program are completely determined during compilation. The figure shows the specific compilation and packaging process of AOT. The application code, dependencies, JDK, etc., on the left are input. GraalVM uses the main function as the entry to scan all reachable codes and execution paths. Some pre-initialization actions may be involved in processing. Finally, the machine code, initialization resources, and other state data compiled by AOT are packaged as executable Native packages.
Compared to the traditional JVM deployment mode, the GraalVM Native Image mode is very different.
• GraalVM uses the main function as the entry during building and compiling to complete static application code analysis.
• Code that cannot be reached during static analysis is removed and not included in the final binary package.
• GraalVM cannot recognize some dynamic calling behaviors in the code, such as reflection, resource loading, serialization, and dynamic proxy, which are all limited.
• Classpath is solidified during building and cannot be modified.
• Delayed class loading is no longer supported, and all available classes and code are determined at the program startup stage.
• Some other Java application capabilities are limited, such as ahead-of-time class initialization.
GraalVM does not support dynamic features like reflection, which are extensively used in many applications and frameworks. To package these applications as Native Images and make them static, GraalVM provides a metadata configuration entry. By providing configuration files for all dynamic features, the "closed world assumption" mode remains valid, allowing GraalVM to know all expected behaviors at compile time.
There are two examples:
1. The encoding method, such as the encoding method of reflection here, allows GraalVM to analyze and calculate metadata through code.
2. Another example is to provide an additional json configuration file and place it under the specified directory: META-INF/native-image//.
Using dynamic features such as reflection in Java applications or frameworks is an obstacle to using GraalVM. However, a large number of frameworks have this limitation. It will be a very challenging task if applications or developers are required to provide metadata configuration. Therefore, frameworks such as Spring and Dubbo introduce AOT Processing before AOT compilation. AOT Processing is used to collect metadata automatically and provide the metadata to the AOT compiler.
The AOT compilation mechanism is common to all Java applications. However, compared to AOT compilation, the process of collecting metadata by AOT Processing is different for each framework because each framework has its own usage for reflection and dynamic proxies.
Let's take a Spring + Dubbo microservices application as an example. To achieve static packaging for this application, it involves the metadata processing of Spring, Dubbo, and third-party dependencies.
• Spring - Spring AOT processing
• Dubbo - Dubbo AOT processing
• Third-party libraries - Reachability Metadata
For Spring, the Spring AOT mechanism was introduced in Spring 6 to support static preprocessing of Spring applications. Similarly, Dubbo released the Dubbo AOT mechanism in version 3.2, enabling automatic preprocessing of Dubbo-related components. Besides these two frameworks closely related to business development, an application often relies on numerous third-party dependencies, and their metadata is crucial for static processing. If these third-party libraries involve behaviors such as reflection and class loading, metadata configuration needs to be provided. Currently, there are two ways to provide metadata configuration for these third-party libraries. One option is to use the shared space provided by GraalVM, which offers a significant portion of metadata configurations for dependencies. The other approach is to require official component releases to include metadata configuration. GraalVM can automatically read metadata in both cases.
Metadata configuration: https://github.com/oracle/graalvm-reachability-metadata
Next, let's look at what Spring AOT has done before compilation. Spring framework has many dynamic features, such as automatic configuration and conditional bean. Spring AOT preprocesses these dynamic features during building to generate a series of metadata inputs that can be used by GraalVM. The output is as follows:
• Pregenerated code related to the Spring Bean definition, as shown in the following figure.
• Dynamic proxy-related code generated during building.
• JSON metadata file used for reflection.
What Dubbo AOT does is similar to Spring AOT, except that Dubbo AOT is used to preprocess the specific usage of the Dubbo framework, including:
• Generate SPI extension-related source code.
• Generate JSON configuration file used for some reflection.
• Generate RPC proxy class code.
Next, I'll use a sample microservices application of Spring6 + Dubbo3 to demonstrate how to use Spring AOT and Dubbo AOT to package the Native Image of an application.
The complete code sample can be downloaded here: https://github.com/apache/dubbo-samples/tree/master/1-basic/dubbo-samples-native-image
This sample application is a common microservice application. We use Spring Boot 3 for application configuration development and Dubbo3 to define and publish RPC services. The application building tool is Maven.
The focus is to add three plug-in configurations: spring-boot-maven-plugin, native-maven-plugin, and dubbo-maven-plugin. Open the AOT processing, and modify the mainClass in the dubbo-maven-plugin as the full path of the required startup class. (You do not need to add spring-boot-maven-plugin dependency for API usage.)
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release>
<fork>true</fork>
<verbose>true</verbose>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.20</version>
<configuration>
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
<requiredVersion>22.3</requiredVersion>
</configuration>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals>
<goal>add-reachability-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-maven-plugin</artifactId>
<version>${dubbo.version}</version>
<configuration>
<mainClass>com.example.nativedemo.NativeDemoApplication</mainClass>
</configuration>
<executions>
<execution>
<phase>process-sources</phase>
<goals>
<goal>dubbo-process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
In addition, for Dubbo, because some Native mechanisms depend on versions such as JDK17, Dubbo does not package some packages into the release version by default, so two additional dependencies need to be added to dubbo-spring6 adaptation and dubbo-native components.
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-config-spring6</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-native</artifactId>
<version>${dubbo.version}</version>
</dependency>
At the same time, this example supports limited third-party components and mainly supports the Reachability Metadata of third-party components. For example, the currently supported network communication or coding components are Netty and Fastjson2, the supported logging component is Logback, and the supported microservice components are Nacos and Zookeeper.
• The serialization method supported well is Fastjson2.
• The compiler and proxy can only choose JDK.
• Logger needs to be configured with slf4j. Currently, only logback is supported.
Example:
dubbo:
application:
name: ${spring.application.name}
logger: slf4j
compiler: jdk
protocol:
name: dubbo
port: -1
serialization: fastjson2
registry:
id: zk-registry
address: zookeeper://127.0.0.1:2181
config-center:
address: zookeeper://127.0.0.1:2181
metadata-report:
address: zookeeper://127.0.0.1:2181
provider:
proxy: jdk
serialization: fastjson2
consumer:
proxy: jdk
serialization: fastjson2
Execute the following compilation command in the root path of the project:
• Direct execution through API
mvn clean install -P native -Dmaven.test.skip=true
• Annotation and XML modes (Integrated mode of Springboot3)
mvn clean install -P native native:compile -Dmaven.test.skip=true
Binary files are in the target/ directory. Generally, the project name is the binary package name, such as target/native-demo.
GraalVM technology has brought new changes to Java applications in the era of cloud computing. It has effectively addressed common issues faced by Java applications, such as slow startup times and excessive resource consumption. However, it's important to acknowledge the limitations associated with using GraalVM. In response to these limitations, Spring 6, Spring Boot 3, and Dubbo 3 have introduced their respective Native solutions. Additionally, Spring Cloud Alibaba is actively promoting static packaging solutions. Going forward, we will focus on conducting comprehensive Native static verification, with a particular emphasis on the surrounding ecosystem components of these two frameworks, including nacos, sentinel, and seata.
[1] Apache Dubbo blog
https://cn.dubbo.apache.org/en/blog/
Resolving "Address not available" Issues in a Container Environment
508 posts | 48 followers
FollowAlibaba Cloud Native Community - January 26, 2024
Alibaba Container Service - April 28, 2020
Alibaba Cloud Native Community - December 8, 2023
Alibaba Clouder - March 17, 2021
Aliware - August 18, 2021
Alibaba Cloud Native Community - August 21, 2023
508 posts | 48 followers
FollowAccelerate and secure the development, deployment, and management of containerized applications cost-effectively.
Learn MoreAlibaba Cloud Function Compute is a fully-managed event-driven compute service. It allows you to focus on writing and uploading code without the need to manage infrastructure such as servers.
Learn MoreLindorm is an elastic cloud-native database service that supports multiple data models. It is capable of processing various types of data and is compatible with multiple database engine, such as Apache HBase®, Apache Cassandra®, and OpenTSDB.
Learn MoreAlibaba Cloud PolarDB for MySQL is a cloud-native relational database service 100% compatible with MySQL.
Learn MoreMore Posts by Alibaba Cloud Native Community