Create a multi-platform container image with Java SE Subscription Enterprise Performance Pack
on June 12, 2023Every Java release ships with numerous features, like JEP 254: Compact Strings in Java 9 and JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production) in Java 15, etc. As these changes are available starting with those releases onward, developers who cannot upgrade their Java applications cannot use those features.
The Java SE Subscription Enterprise Performance Pack (EPP) brings the improvements available to Java 17 to the Java 8 family of applications. Java EPP is intended to be a drop-in replacement for any Java 8 backend application. Customers who have adopted Java EPP saw considerable gains in the form of reduced garbage collection time, lower memory footprint, and better throughput.
As container images are a common way to run modern applications, this article aims to show you how to create a multi-platform Java EPP container image compatible with both the amd64 and the arm64 architectures. Using this approach, you can run the same Java code on different architectures, thus enabling you to move the workload to the platform that offers the best performance.
Prerequisites
The Java EPP is included in the Oracle Java SE Subscription and is also available for download under the Java SE OTN License for development, prototyping, etc. To try the examples showed here, you need the following:
- An Oracle account to download the Java EPP binaries under the OTN license or an Oracle Java SE Subscription.
- A docker compatible build tool that support creation of multi-platform container images, such as the buildx and BuildKit.
- A container registry where to push the container image. This example uses a custom container registry created in OCI, but any container registry will do.
Download the Java EPP binaries
You can download the Java EPP binaries from the following places:
You should download the .tar.gz
file for each architecture (amd64 and arm64) and place them in a directory
called binaries
. You can use tree
command to validate the presence of the binaries in the ./binaries
directory:
$ tree './binaries'
./binaries
├── jdk-8u371-perf-linux-aarch64.tar.gz
└── jdk-8u371-perf-linux-x64.tar.gz
NOTE
The Java EPP binaries have a distinctive -perf
in their file name.
You do not need to extract these binary files. These files be copied into the container images and extracted during the container image creation.
Produce the application JAR files
Create the application JAR file using your preferred build tool, be it Gradle, Maven, Ant etc. You can package the application as either a single fat JAR file or as multiple JAR files.
Different build tools create the JAR file (and copy the dependencies) in different default output directories. For
example, Gradle uses the ./build/libs
directory while Maven uses the ./target
directory as their respective output
directories. In order to be agnostic of the build tool, we will assume that all application JAR files are in
the ./jars
directory.
In this example, the application main class is in package demo
and we will start the application using the command:
$ java -classpath './jars/*' demo.Main
Create a multi-architecture Dockerfile
Container images are a common way to package, distribute, and run modern applications. Developers package their code with all its dependencies into a container image and distribute it over container registries, such as the Oracle Container Registry, to become directly accessible to the container hosting environment at runtime. The users of the container image do not have to worry about how to configure the application or what version of Java to be used, as everything is encapsulated inside the container image.
To simplify adoption of a technology across different architectures, many container images are built with multi-platform
support, like the container-registry.oracle.com/java/jdk-no-fee-term:17.0.7-oraclelinux8
container image.
Docker streamlined multi-platform support through
its buildx plugin
and --platform
option. Using these
features, users of our container image can point to the same tag and docker will then choose the right container image
for their architecture.
For example, the JDK 17 container image has multi-platform support and is available for both the amd64 and arm64
architectures.
You can use inspect option to check all supported architectures under an image tag:
$ docker buildx imagetools \
inspect container-registry.oracle.com/java/jdk-no-fee-term:17.0.7-oraclelinux8
Name: container-registry.oracle.com/java/jdk-no-fee-term:17.0.7-oraclelinux8
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:6eb6accdd2afb3118d6487b358a928ec9c6d3fb9abd30107c9f15d0e05634e18
Manifests:
Name: container-registry.oracle.com/java/jdk-no-fee-term:17.0.7-oraclelinux8@sha256:d93847cfa8a7e66ea7fea7b6aaf12b225e5e4c9fdef3a6a3544c8ce3aaa79acc
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm64
Name: container-registry.oracle.com/java/jdk-no-fee-term:17.0.7-oraclelinux8@sha256:ff56f1ff813ff71e5bbe2804e1eedd3cd42983e87e3b5ff8caf8a62da6bf9027
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/amd64
As we aim to support both amd64 and the arm64 architectures, we will use multi-stage docker builds where we define an intermediate stage for each architecture. This is similar to Java polymorphism as the docker daemon will use the right stage depending on the target architecture. The Oracle Linux 9 base image will be used as base image in our example, but you can choose any 64-bit Linux distribution that supports both amd64 and the arm64 architectures.
NOTE You should be aware that Java EPP runs only on 64-bit Linux.
The Dockerfile
contains four stages, and we will build this file one stage at a time, describing each stage as we go
along. Please feel free to skip ahead to see the final example.
1⋅ Create the base stage that contains just the operating system
FROM container-registry.oracle.com/os/oraclelinux:9 AS base
WORKDIR /opt
The container image build tool will use the correct container image based on the desired architecture. For example, when building the container image for amd64 architecture, the amd64 version of the OS is used.
2⋅ Add a stage for the arm64 architecture that extends the base OS stage
In this stage we will carry out only the configuration needed by the arm64 architecture. Copy the arm64 Java EPP
binary (./binaries/jdk-8u371-perf-linux-aarch64.tar.gz
) and extract it into the /opt/jdk
directory.
FROM base AS base-arm64
COPY ./binaries/jdk-8u371-perf-linux-aarch64.tar.gz jdk-8u371-perf-linux-aarch64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-aarch64.tar.gz \
&& rm jdk-8u371-perf-linux-aarch64.tar.gz \
&& mv jdk1.8.0_371 jdk
You should update the path if you have saved the jdk-8u371-perf-linux-aarch64.tar.gz
binary file elsewhere.
Note that the stage name, base-arm64
, contains the architecture name in it. When building the arm64 variant of
this container image, the container image build tool will pick this stage based on its name.
Our Dockerfile
now contains two stages.
FROM container-registry.oracle.com/os/oraclelinux:9 AS base
WORKDIR /opt
FROM base AS base-arm64
COPY ./binaries/jdk-8u371-perf-linux-aarch64.tar.gz jdk-8u371-perf-linux-aarch64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-aarch64.tar.gz \
&& rm jdk-8u371-perf-linux-aarch64.tar.gz \
&& mv jdk1.8.0_371 jdk
3⋅ Add a stage for the amd64 architecture that extends the base OS stage
This is similar to the previous stage, but it has two key differences. This stage is named base-amd64
and it uses
the amd64 Java EPP binary (./binaries/jdk-8u371-perf-linux-aarch64.tar.gz
) instead.
FROM base AS base-amd64
COPY ./binaries/jdk-8u371-perf-linux-x64.tar.gz jdk-8u371-perf-linux-x64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-x64.tar.gz \
&& rm jdk-8u371-perf-linux-x64.tar.gz \
&& mv jdk1.8.0_371 jdk
In both stages, docker daemon extracts Java EPP into the /opt/jdk
directory. While this is not a must, extracting
both versions in the same directory path simplifies the subsequent stages as these can assume that Java is found
under the /opt/jdk
directory.
The Dockerfile
now has three stages.
FROM container-registry.oracle.com/os/oraclelinux:9 AS base
WORKDIR /opt
FROM base AS base-arm64
COPY ./binaries/jdk-8u371-perf-linux-aarch64.tar.gz jdk-8u371-perf-linux-aarch64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-aarch64.tar.gz \
&& rm jdk-8u371-perf-linux-aarch64.tar.gz \
&& mv jdk1.8.0_371 jdk
FROM base AS base-amd64
COPY ./binaries/jdk-8u371-perf-linux-x64.tar.gz jdk-8u371-perf-linux-x64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-x64.tar.gz \
&& rm jdk-8u371-perf-linux-x64.tar.gz \
&& mv jdk1.8.0_371 jdk
Up to now we have a generic multi-platform container image that uses Java EPP.
NOTE
JAVA_HOME
environment variable is not set and the java
executable file is not on the PATH
(it will be configured
in the following stage).
An alternative approach is to insert another stage between this stage and the next stage where we define the JAVA_HOME
environment variable and put the java
executable file on the PATH
.
4⋅ Create the final stage that extends one of the previous stages at build time
The docker buildx plugin
provides several platform arguments (ARG
s) in the global scope,
such as TARGETARCH
. Please keep in mind that these are only available when using the docker buildx plugin.
The argument TARGETARCH
contains the name of target architecture, such as amd64
and arm64
:
FROM base-${TARGETARCH}
When building a container image for the amd64 architecture, the above instruction evaluates to
FROM base-amd64
A similar evaluation happens when building a container image for the arm64 architecture.
The rest of the stage is fairly standard.
FROM base-${TARGETARCH}
WORKDIR /opt/app
ENV JAVA_HOME "/opt/jdk"
ENV PATH "${PATH}:${JAVA_HOME}/bin"
COPY ./jars .
ENTRYPOINT ["java", "-classpath", "./*", "demo.Main"]
The final Dockerfile
looks similar to:
FROM container-registry.oracle.com/os/oraclelinux:9 AS base
WORKDIR /opt
FROM base AS base-arm64
COPY ./binaries/jdk-8u371-perf-linux-aarch64.tar.gz jdk-8u371-perf-linux-aarch64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-aarch64.tar.gz \
&& rm jdk-8u371-perf-linux-aarch64.tar.gz \
&& mv jdk1.8.0_371 jdk
FROM base AS base-amd64
COPY ./binaries/jdk-8u371-perf-linux-x64.tar.gz jdk-8u371-perf-linux-x64.tar.gz
RUN tar xvfz jdk-8u371-perf-linux-x64.tar.gz \
&& rm jdk-8u371-perf-linux-x64.tar.gz \
&& mv jdk1.8.0_371 jdk
FROM base-${TARGETARCH}
WORKDIR /opt/app
ENV JAVA_HOME "/opt/jdk"
ENV PATH "${PATH}:${JAVA_HOME}/bin"
COPY ./jars .
ENTRYPOINT ["java", "-classpath", "./*", "demo.Main"]
We can proceed to building this container image.
Build and publish the multi-platform container image
As mentioned in the prerequisites section, you will need the following:
- A buildx context that supports the amd64 and arm64 architectures
- A container registry where the image will be published
First, you should verify the existence of a buildx context that supports both the amd64 and arm64 architectures. You
can list all available using the docker buildx ls
command:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
...
multi-platform-builder * docker-container
multi-platform-builder0 unix:///~/.colima/docker.sock running v0.11.5 linux/arm64, linux/amd64, linux/amd64/v2
...
If a context is not available, you can create one that supports both linux/arm64
and linux/amd64
using
docker buildx create command:
$ docker buildx create \
--name multi-platform-builder \
--driver docker-container \
--bootstrap
Set the new context as the current build context with docker buildx use
command:
$ docker buildx use multi-platform-builder
Finally, build the container image targeting both linux/amd64
and linux/arm64
platforms and push it to your
container registry, having the semantic version 1.0.0
:
$ docker build \
--platform linux/amd64,linux/arm64 \
--tag iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0 \
--push
.
Inspect the newly created container image and verify that both the amd64 and the arm64 architectures are supported:
$ docker buildx imagetools \
inspect iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0
Name: iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:870e5bd6d83a718a17c460fc9b25741295b581c0f321ffe9371ef827c35121a8
Manifests:
Name: iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0@sha256:3c8669dd275f8dd556c33b1b75890b21d690a5c3bd32879a6a92090a3bccca71
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Name: iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0@sha256:bde47574310fcca1b29122d592dee291674540fc6083a0b03debfae9d2ef9ed9
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/arm64
...
Once you published the container image, you can run it:
$ docker run iad.ocir.io/xxxxxxxxxxxx/epp_multi_platform_containers:1.0.0
Final Thoughts
This article combines the Java EPP together with multi-platform container images to enable delivery of high performing Java 8 applications using modern distribution channels. This approach enables you to run your Java 8 applications with the Java 17 benefits via Java EPP.