openjdk Archives - Piotr's TechBlog https://piotrminkowski.com/tag/openjdk/ Java, Spring, Kotlin, microservices, Kubernetes, containers Tue, 07 Nov 2023 21:26:28 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.1 https://i0.wp.com/piotrminkowski.com/wp-content/uploads/2020/08/cropped-me-2-tr-x-1.png?fit=32%2C32&ssl=1 openjdk Archives - Piotr's TechBlog https://piotrminkowski.com/tag/openjdk/ 32 32 181738725 Slim Docker Images for Java https://piotrminkowski.com/2023/11/07/slim-docker-images-for-java/ https://piotrminkowski.com/2023/11/07/slim-docker-images-for-java/#comments Tue, 07 Nov 2023 21:26:26 +0000 https://piotrminkowski.com/?p=14643 In this article, you will learn how to build slim Docker images for your Java apps using Alpine Linux and the jlink tool. We will leverage the latest Java 21 base images provided by Eclipse Temurin and BellSoft Liberica. We are going to compare those providers with Alpaquita Linux also delivered by BellSoft. That comparison […]

The post Slim Docker Images for Java appeared first on Piotr's TechBlog.

]]>
In this article, you will learn how to build slim Docker images for your Java apps using Alpine Linux and the jlink tool. We will leverage the latest Java 21 base images provided by Eclipse Temurin and BellSoft Liberica. We are going to compare those providers with Alpaquita Linux also delivered by BellSoft. That comparison will also include security scoring based on the number of vulnerabilities. As an example, we will use a simple Spring Boot app that exposes some REST endpoints.

If you are interested in Java in the containerization context you may find some similar articles on my blog. For example, you can read how to speed up Java startup on Kubernetes with CRaC in that post. There is also an article comparing different JDK providers used for running the Java apps by Paketo Buildpacks.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you need to go to the spring-microservice directory. After that, you should just follow my instructions.

Introduction

I probably don’t need to convince anyone that keeping Docker images slim and light is important. It speeds up the build process and deployment of containers. Decreasing image size and removing unnecessary files eliminate vulnerable components and therefore reduce the risk of security issues. Usually, the first step to reduce the target image size is to choose a small base image. Our choice will not be surprising – Alpine Linux. It is a Linux distribution built around musl libc and BusyBox. The image has only 5 MB.

Also, Java in itself consumes some space inside the image. Fortunately, we can reduce that size by using the jlink tool. With jlink we can choose only the modules required by our app, and link them into a runtime image. Our main goal today is to create as small as possible Docker image for our sample Spring Boot app.

Sample Spring Boot App

As I mentioned before, our Java app is not complicated. It uses Spring Boot Web Starter to expose REST endpoints over HTTP. I made some small improvements in the dependencies. Tomcat has been replaced with Undertow to reduce the target JAR file size. I also imported the latest version of the org.yaml:snakeyaml library to avoid a CVE issue related to the 1.X release of that project. Of course, I’m using Java 21 for compilation:

<properties>
  <java.version>21</java.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>2.2</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-undertow</artifactId>
  </dependency>
</dependencies>

Here’s the implementation of the @RestController responsible for exposing several endpoints:

@RestController
@RequestMapping("/persons")
public class Api {

   protected Logger logger = Logger.getLogger(Api.class.getName());

   private List<Person> persons;

   public Api() {
      persons = new ArrayList<>();
      persons.add(new Person(1, "Jan", "Kowalski", 22));
      persons.add(new Person(2, "Adam", "Malinowski", 33));
      persons.add(new Person(3, "Tomasz", "Janowski", 25));
      persons.add(new Person(4, "Alina", "IksiƄska", 54));
   }

   @GetMapping
   public List<Person> findAll() {
      logger.info("Api.findAll()");
      return persons;
   }

   @GetMapping("/{id}")
   public Person findById(@PathVariable("id") Integer id) {
      logger.info(String.format("Api.findById(%d)", id));
      return persons.stream()
                    .filter(p -> (p.getId().intValue() == id))
                    .findAny()
                    .orElseThrow();
   }

}

In the next step, we will prepare and build several Docker images for our Java app and compare them with each other.

Build Alpine Image with BellSoft Liberica OpenJDK

Let’s take a look at the Dockerfile. We are using a feature called multi-stage Docker builds. In the first step, we are the Java runtime for our app (1). We download and unpack the latest LTS version of OpenJDK from BellSoft (2). We need a release targeted for Alpine Linux (with the musl suffix). Then, we are running the jlink command to create a custom image with JDK (3). In order to run the app, we need to include at least the following Java modules: java.base, java.logging, java.naming, java.desktop, jdk.unsupported (4). You can verify a list of required modules by running the jdeps command e.g. on your JAR file. The jlink tool will place our custom JDK runtime in the springboot-runtime directory (the --output parameter).

Finally, we can proceed to the main phase of the image build (5). We are placing the optimized version of JDK in the /opt/jdk path by copying it from the directory created during the previous build phase (6). Then we are just running the app using the java -jar command.

# (1)
FROM alpine:latest AS build 
ENV JAVA_HOME /opt/jdk/jdk-21.0.1
ENV PATH $JAVA_HOME/bin:$PATH

# (2)
ADD https://download.bell-sw.com/java/21.0.1+12/bellsoft-jdk21.0.1+12-linux-x64-musl.tar.gz /opt/jdk/
RUN tar -xzvf /opt/jdk/bellsoft-jdk21.0.1+12-linux-x64-musl.tar.gz -C /opt/jdk/

# (3)
RUN ["jlink", "--compress=2", \
     "--module-path", "/opt/jdk/jdk-21.0.1/jmods/", \
# (4)
     "--add-modules", "java.base,java.logging,java.naming,java.desktop,jdk.unsupported", \
     "--no-header-files", "--no-man-pages", \
     "--output", "/springboot-runtime"]

# (5)
FROM alpine:latest
# (6)
COPY --from=build  /springboot-runtime /opt/jdk 
ENV PATH=$PATH:/opt/jdk/bin
EXPOSE 8080
COPY ../target/spring-microservice-1.0-SNAPSHOT.jar /opt/app/
CMD ["java", "-showversion", "-jar", "/opt/app/spring-microservice-1.0-SNAPSHOT.jar"]

Let’s build the image by executing the following command. We are tagging the image with bellsoft and preparing it for pushing to the quay.io registry:

$ docker build -t quay.io/pminkows/spring-microservice:bellsoft . 

Here’s the result:

We can examine the image using the dive tool. If you don’t have any previous experience with dive CLI you can read more about it here. We need to run the following command to analyze the current image:

$ dive quay.io/pminkows/spring-microservice:bellsoft

Here’s the result. As you see our image has 114MB. Java is consuming 87 MB, the app JAR file 20MB, and Alpine Linux 7.3.MB. You can also take a look at the list of modules and the whole directory structure.

docker-images-java-dive

In the end, let’s push our image to the Quay registry. Quay will automatically perform a security scan of the image. We will discuss it later.

$ docker push quay.io/pminkows/spring-microservice:bellsoft

Build Alpine Image with Eclipse Temurin OpenJDK

Are you still not satisfied with the image size? Me too. I expected something below 100MB. Let’s experiment a little bit. I will use almost the same Dockerfile as before, but instead of BellSoft Liberica, I will download and optimize the Eclipse Temurin OpenJDK for Alpine Linux. Here’s the current Dockerfile. As you see the only difference is in the JDK URL.

FROM alpine:latest AS build
ENV JAVA_HOME /opt/jdk/jdk-21.0.1+12
ENV PATH $JAVA_HOME/bin:$PATH

ADD https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.1%2B12/OpenJDK21U-jdk_x64_alpine-linux_hotspot_21.0.1_12.tar.gz /opt/jdk/
RUN tar -xzvf /opt/jdk/OpenJDK21U-jdk_x64_alpine-linux_hotspot_21.0.1_12.tar.gz -C /opt/jdk/
RUN ["jlink", "--compress=2", \
     "--module-path", "/opt/jdk/jdk-21.0.1+12/jmods/", \
     "--add-modules", "java.base,java.logging,java.naming,java.desktop,jdk.unsupported", \
     "--no-header-files", "--no-man-pages", \
     "--output", "/springboot-runtime"]

FROM alpine:latest
COPY --from=build  /springboot-runtime /opt/jdk
ENV PATH=$PATH:/opt/jdk/bin
EXPOSE 8080
COPY ../target/spring-microservice-1.0-SNAPSHOT.jar /opt/app/
CMD ["java", "-showversion", "-jar", "/opt/app/spring-microservice-1.0-SNAPSHOT.jar"]

The same as before, we will build the image. This time we are tagging it with temurin. We also need to override the default location of the Docker manifest since we use the Dockerfile_temurin:

$ docker build -f Dockerfile_temurin \
    -t quay.io/pminkows/spring-microservice:temurin .

Once the image is ready we can proceed to the next steps:

Let’s analyze it with the dive tool:

$ dive quay.io/pminkows/spring-microservice:temurin

The results look much better. The difference is of course in the JDK space. It took just 64MB instead of 87MB like in Liberica. The total image size is 91MB.

Finally, let’s push the image to the Quay registry for the security score comparison:

$ docker push quay.io/pminkows/spring-microservice:temurin

Build Image with BellSoft Alpaquita

BellSoft Alpaquita is a relatively new solution introduced in 2022. It is advertised as a full-featured operating system optimized for Java. We can use Alpaquita Linux in combination with Liberica JDK Lite. This time we won’t create a custom JDK runtime, but we will get the ready image provided by BellSoft in their registry: bellsoft/liberica-runtime-container:jdk-21-slim-musl. It is built on top of Alpaquita Linux. Here’s our Dockerfile:

FROM bellsoft/liberica-runtime-container:jdk-21-slim-musl
COPY target/spring-microservice-1.0-SNAPSHOT.jar /opt/app/
EXPOSE 8080
CMD ["java", "-showversion", "-jar", "/opt/app/spring-microservice-1.0-SNAPSHOT.jar"]

Let’s build the image. The current Docker manifest is available in the repository as the Dockerfile_alpaquita file:

$ docker build -f Dockerfile_alpaquita \
    -t quay.io/pminkows/spring-microservice:alpaquita .

Here’s the build result:

Let’s examine our image with dive once again. The current image has 125MB. Of course, it is more than two previous images, but still not much.

Finally, let’s push the image to the Quay registry for the security score comparison:

$ docker push quay.io/pminkows/spring-microservice:alpaquita

Now, we can switch to the quay.io. In the repository view, we can compare the results of security scanning for all three images. As you see, there are no detected vulnerabilities for the image tagged with alpaquita and two issues for another two images.

docker-images-java-quay

Paketo Buildpacks for Alpaquita

BellSoft provides a dedicated buildpack based on the Alpaquita image. As you probably know, Spring Boot offers the ability to integrate the build process with Paketo Buildpacks through the spring-boot-maven-plugin. The plugin configuration in Maven pom.xml is visible below. We need to the bellsoft/buildpacks.builder:musl as a builder image. We can also enable jlink optimization by setting the environment variable BP_JVM_JLINK_ENABLED to true. In order to make the build work, I need to decrease the Java version to 17.

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <name>quay.io/pminkows/spring-microservice:alpaquita-pack</name>
      <builder>bellsoft/buildpacks.builder:musl</builder>
      <env>
        <BP_JVM_VERSION>17</BP_JVM_VERSION>
        <BP_JVM_JLINK_ENABLED>true</BP_JVM_JLINK_ENABLED>
        <BP_JVM_JLINK_ARGS>--no-man-pages --no-header-files --strip-debug --compress=2 --add-modules java.base,java.logging,java.naming,java.desktop,jdk.unsupported</BP_JVM_JLINK_ARGS>
      </env>
    </image>
  </configuration>
</plugin>

Let’s build the image with the following command:

$ mvn clean spring-boot:build-image

You should have a similar output if everything finishes successfully:

docker-images-java-buildpacks

After that, we can examine the image with the dive CLI. I was able to get an even better image size than for the corresponding build based on the Dockerfile with an alpine image with BellSoft Liberica OpenJDK (103MB vs 114MB). However, now I was using the JDK 17 instead of JDK 21 as in the previous build.

Finally, let’s push the image to the Quay registry:

 $ docker push quay.io/pminkows/spring-microservice:alpaquita-pack

Security Scans of Java Docker Images

We can use a more advanced tool for security scanning than Quay. Personally, I’m using Advanced Cluster Security for Kubernetes. It can be used not only to monitor containers running on the Kubernetes cluster but also to watch particular images in the selected registry. We can add all our previously built images in the “Manage watched images” section.

Here’s the security report for all our Docker Java images. It looks very good. There is only one security issue detected for both images based on alpine. There are no any CVEs fond for alpaquita-based images.

docker-images-java-acs

We can get into the details of every CVE. The issue detected for both images tagged with temurin and bellsoft is related to the jackson-databind Java library used by the Spring Web dependency.

Final Thoughts

As you see we can easily create slim Docker images for the Java apps without any advanced tools. The size of such an image can be even lower than 100MB (including ~20MB JAR file). BellSoft Alpaquita is also a very interesting alternative to Linux Alpine. We can use it with Paketo Buildpacks and take advantage of Spring Boot support for building images with CNB.

The post Slim Docker Images for Java appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/11/07/slim-docker-images-for-java/feed/ 8 14643
Which JDK to Choose on Kubernetes https://piotrminkowski.com/2023/02/17/which-jdk-to-choose-on-kubernetes/ https://piotrminkowski.com/2023/02/17/which-jdk-to-choose-on-kubernetes/#comments Fri, 17 Feb 2023 13:53:19 +0000 https://piotrminkowski.com/?p=14015 In this article, we will make a performance comparison between several most popular JDK implementations for the app running on Kubernetes. This post also answers some questions and concerns about my Twitter publication you see below. I compared Oracle JDK with Eclipse Temurin. The result was quite surprising for me, so I decided to tweet […]

The post Which JDK to Choose on Kubernetes appeared first on Piotr's TechBlog.

]]>
In this article, we will make a performance comparison between several most popular JDK implementations for the app running on Kubernetes. This post also answers some questions and concerns about my Twitter publication you see below. I compared Oracle JDK with Eclipse Temurin. The result was quite surprising for me, so I decided to tweet to get some opinions and feedback.

jdk-kubernetes-tweet

Unfortunately, those results were wrong. Or maybe I should say, were not averaged well enough. After this publication, I also received interesting materials presented on London Java Community. It compares the performance of the Payara application server running on various JDKs. Here’s the link to that presentation (~1h). The results showed there seem to confirm my results. Or at least they confirm the general rule – there are some performance differences between Open JDK implementations. Let’s check it out.

This time I’ll do a very accurate comparison with several repeats to get reproducible results. I’ll test the following JVM implementations:

  • Adoptium Eclipse Temurin
  • Alibaba Dragonwell
  • Amazon Corretto
  • Azul Zulu
  • BellSoft Liberica
  • IBM Semeru OpenJ9
  • Oracle JDK
  • Microsoft OpenJDK

For all the tests I’ll use Paketo Java buildpack. We can easily switch between several JVM implementations with Paketo. I’ll test a simple Spring Boot 3 app that uses Spring Data to interact with the Mongo database. Let’s proceed to the details!

If you have already built images with Dockerfile it is possible that you were using the official OpenJDK base image from the Docker Hub. However, currently, the announcement on the image site says that it is officially deprecated and all users should find suitable replacements. In this article, we will compare all the most popular replacements, so I hope it may help you to make a good choice 🙂

Testing Environment

Before we run tests it is important to have a provisioned environment. I’ll run all the tests locally. In order to build images, I’m going to use Paketo Buildpacks. Here are some details of my environment:

  1. Machine: MacBook Pro 32G RAM Intel 
  2. OS: macOS Ventura 13.1
  3. Kubernetes (v1.25.2) on Docker Desktop: 14G RAM + 4vCPU

We will use Java 17 for app compilation. In order to run load tests, I’m going to leverage the k6 tool. Our app is written in Spring Boot. It connects to the Mongo database running on the same instance of Kubernetes. Each time I’m testing a new JVM provider I’m removing the previous version of the app and database. Then I’m deploying the new, full configuration once again. We will measure the following parameters:

  1. App startup time (the best  
  2. result and average) – we will read it directly from the Spring Boot logs 
  3. Throughput – with k6 we will simulate 5 and 10 virtual users. It will measure the number of processing requests 
  4. The size of the image
  5. The RAM memory consumed by the pod during the load tests. Basically, we will execute the kubectl top pod command

We will also set the memory limit for the container to 1G. In our load tests, the app will insert data into the Mongo database. It is exposing the REST endpoint invoked during the tests. To measure startup time as accurately as possible I’ll restart the app several times.

Let’s take a look at the Deployment YAML manifest. It injects credentials to the Mongo database and set the memory limit to 1G (as I already mentioned):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-spring-boot-on-kubernetes-deployment
spec:
  selector:
    matchLabels:
      app: sample-spring-boot-on-kubernetes
  template:
    metadata:
      labels:
        app: sample-spring-boot-on-kubernetes
    spec:
      containers:
      - name: sample-spring-boot-on-kubernetes
        image: piomin/sample-spring-boot-on-kubernetes
        ports:
        - containerPort: 8080
        env:
          - name: MONGO_DATABASE
            valueFrom:
              configMapKeyRef:
                name: mongodb
                key: database-name
          - name: MONGO_USERNAME
            valueFrom:
              secretKeyRef:
                name: mongodb
                key: database-user
          - name: MONGO_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mongodb
                key: database-password
          - name: MONGO_URL
            value: mongodb
        readinessProbe:
          httpGet:
            port: 8080
            path: /readiness
            scheme: HTTP
          timeoutSeconds: 1
          periodSeconds: 10
          successThreshold: 1
          failureThreshold: 3
        resources:
          limits:
            memory: 1024Mi

Source Code and Images

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. You will also find all the images in my Docker Hub repository piomin/sample-spring-boot-on-kubernetes. Every single image is tagged with the vendor’s name.

Our Spring Boot app exposes several endpoints, but I’ll test the POST /persons endpoint for inserting data into Mongo. In the integration with Mongo, I’m using the Spring Data MongoDB project and its CRUD repository pattern.

// controller

@RestController
@RequestMapping("/persons")
public class PersonController {

   private PersonRepository repository;

   PersonController(PersonRepository repository) {
      this.repository = repository;
   }

   @PostMapping
   public Person add(@RequestBody Person person) {
      return repository.save(person);
   }

   // other endpoints implementation
}


// repository

public interface PersonRepository extends CrudRepository<Person, String> {

   Set<Person> findByFirstNameAndLastName(String firstName, 
                                          String lastName);
   Set<Person> findByAge(int age);
   Set<Person> findByAgeGreaterThan(int age);

}

The Size of the Image

The size of the image is the simplest option to measure. If you would like to check what is exactly inside the image you can use the dive tool. The difference in the size between vendors results from the number of java tools and binaries included inside. From my perspective, the smaller the size the better. I’d rather not use anything that is inside the image. Of course, except all the staff required to run my app successfully. But you may have a different case. Anyway, here’s the content of the app for the Oracle JDK after executing the dive piomin/sample-spring-boot-on-kubernetes:oracle command. As you see, JDK takes up most of the space.

jdk-kubernetes-dive

On the other hand, we can analyze the smallest image. I think it explains the differences in image size since Zulu contains JRE, not the whole JDK.

Here are the result ordered from the smallest image to the biggest.

  • Azul Zulu: 271MB
  • IBM Semeru OpenJ9: 275MB
  • Eclipse Temurin: 286MB
  • BellSoft Liberica: 286MB
  • Oracle OpenJDK: 446MB
  • Alibaba Dragonwell: 459MB
  • Microsoft OpenJDK: 461MB
  • Amazon Corretto: 463MB

Let’s visualize our first results. I think it excellent shows which image contains JDK and which JRE.

jdk-kubernetes-memory

Startup Time

Honestly, it is not very easy to measure a startup time, since the difference between the vendors is not large. Also, the subsequent results for the same provider may differ a lot. For example, on the first try the app starts in 5.8s and after the pod restart 8.4s. My methodology was pretty simple. I restarted the app several times for each JDK provider to measure the average startup time and the fastest startup in the series. Then I repeated the same exercise again to verify if the results are repeatable. The proportions between the first and second series of startup time between corresponding vendors were similar. In fact, the difference between the fastest and the slowest average startup time is not large. I get the best result for Eclipse Temurin (7.2s) and the worst for IBM Semeru OpenJ9 (9.05s).

Let’s see the full list of results. It shows the average startup time of the application from the fastest one.

  • Eclipse Temurin: 7.20s
  • Oracle OpenJDK: 7.22s
  • Amazon Corretto: 7.27s
  • BellSoft Liberica: 7.44s
  • Oracle OpenJDK: 7.77s
  • Alibaba Dragonwell: 8.03s
  • Microsoft OpenJDK: 8.18s
  • IBM Semeru OpenJ9: 9.05s

Once again, here’s the graphical representation of our results. The differences between vendors are sometimes rather cosmetic. Maybe, if the same exercise once again from the beginning the results would be quite different.

jdk-kubernetes-startup

As I mentioned before, I also measured the fastest attempt. This time the best top 3 are Eclipse Temurin, Amazon Corretto, and BellSoft Liberica.

  • Eclipse Temurin: 5.6s
  • Amazon Corretto: 5.95s
  • BellSoft Liberica: 6.05s
  • Oracle OpenJDK: 6.1s
  • Azul Zulu: 6.2s
  • Alibaba Dragonwell: 6.45s
  • Microsoft OpenJDK: 6.9s
  • IBM Semero OpenJ9: 7.85s

Memory

I’m measuring the memory usage of the app under the heavy load with a test simulating 10 users continuously sending requests. It gives me a really large throughput at the level of the app – around 500 requests per second. The results are in line with the expectations. Almost all the vendors have very similar memory usage except IBM Semeru, which uses OpenJ9 JVM. In theory, OpenJ9 should also give us a better startup time. However, in my case, the significant difference is just in the memory footprint. For IBM Semeru the memory usage is around 135MB, while for other vendors it varies in the range of 210-230MB.

  • IBM Semero OpenJ9: 135M
  • Oracle OpenJDK: 211M
  • Azul Zulu: 215M
  • Alibaba DragonwellOracle OpenJDK: 216M
  • BellSoft Liberica: 219M
  • Microsoft OpenJDK: 219M
  • Amazon Corretto: 220M
  • Eclipse Temurin: 230M

Here’s the graphical visualization of our results:

Throughput

In order to generate high incoming traffic to the app I used the k6 tool. It allows us to create tests in JavaScript. Here’s the implementation of our test. It is calling the HTTP POST /persons endpoint with input data in JSON. Then it verifies if the request has been successfully processed on the server side.

import http from 'k6/http';
import { check } from 'k6';

export default function () {

  const payload = JSON.stringify({
      firstName: 'aaa',
      lastName: 'bbb',
      age: 50,
      gender: 'MALE'
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res = http.post(`http://localhost:8080/persons`, payload, params);

  check(res, {
    'is status 200': (res) => res.status === 200,
    'body size is > 0': (r) => r.body.length > 0,
  });
}

Here’s the k6 command for running our test. It is possible to define the duration and number of simultaneous virtual users. In the first step, I’m simulating 5 virtual users:

$ k6 run -d 90s -u 5 load-tests.js

Then, I’m running the tests for 10 virtual users twice per vendor.

$ k6 run -d 90s -u 10 load-tests.js

Here are the sample results printed after executing the k6 test:

I repeated the exercise per the JDK vendor. Here are the throughput results for 5 virtual users:

  • BellSoft Liberica: 451req/s
  • Amazon Corretto: 433req/s
  • IBM Semeru OpenJ9: 432req/s
  • Oracle OpenJDK: 420req/s
  • Microsoft OpenJDK: 418req/s
  • Azul Zulu: 414req/s
  • Eclipse Temurin: 407req/s
  • Alibaba Dragonwell: 405req/s

Here are the throughput results for 10 virtual users:

  • Eclipse Temurin: 580req/s
  • Azul Zulu: 567req/s
  • Microsoft OpenJDK: 561req/s
  • Oracle OpenJDK: 561req/s
  • IBM Semeru OpenJ9: 552req/s
  • Amazon Corretto: 552req/s
  • Alibaba Dragonwell: 551req/s
  • BellSoft Liberica: 540req/s

Final Thoughts

After repeating the load tests several times I need to admit that there are no significant differences in performance between all JDK vendors. We were using the same JVM settings for testing (set by the Paketo Buildpack). Probably, the more tests I will run, the results between different vendors would be even more similar. So, in summary, the results from my tweet have not been confirmed. Ok, so let’s back to the question – which JDK to choose on Kubernetes?

Probably it somehow depends on where you are running your cluster. If for example, it’s Kubernetes EKS on AWS it’s worth using Amazon Corretto. However, if you are looking for the smallest image size you should choose between Azul Zulu, IBM Semeru, BellSoft Liberica, and Adoptium Eclipse Temurin. Additionally, IBM Semeru will consume significantly less memory than other distributions, since it is built on top of OpenJ9.

Don’t forget about best practices when deploying Java apps on Kubernetes. Here’s my article about it.

The post Which JDK to Choose on Kubernetes appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2023/02/17/which-jdk-to-choose-on-kubernetes/feed/ 18 14015
New Developer Friendly Features After Java 8 https://piotrminkowski.com/2021/02/01/new-developer-friendly-features-after-java-8/ https://piotrminkowski.com/2021/02/01/new-developer-friendly-features-after-java-8/#comments Mon, 01 Feb 2021 16:58:54 +0000 https://piotrminkowski.com/?p=9400 In this article, I’m going to describe the most significant and developer-friendly features of Java since the 8th version. Why such an idea? You can find many articles with a list of new features per each Java version on the Web. However, there is a lack of articles that give you a brief summary of […]

The post New Developer Friendly Features After Java 8 appeared first on Piotr's TechBlog.

]]>
In this article, I’m going to describe the most significant and developer-friendly features of Java since the 8th version. Why such an idea? You can find many articles with a list of new features per each Java version on the Web. However, there is a lack of articles that give you a brief summary of the most important changes since the 8th version. Ok, but why the 8th version? Surprisingly, it is still the most commonly used version of Java. And all this even though we are on the eve of the Java 16 release. You can take a look at the results of my survey on Twitter. As you see more than 46% of responders still use Java 8 in production. By contrast, only less than 10% of responders use Java 12 or later.

Of course, Java 8 has introduced a solid pack of changes including Lambda Expressions. After that there was no similar release with such a huge set of key features. Nevertheless, you will find some interesting new features since Java 8. I have already published all the examples with them on Twitter in a graphical form visible below. You may find them using the #AfterJava8 hashtag.

java-new-features-twitter

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. I’m using Maven with Java 15 for compilation.

Switch Expression (JDK 12)

With the Switch expression, you can define multiple case labels and return values using an arrow. This feature is available since JDK 12. It makes the Switch expression really more accessible.

public String newMultiSwitch(int day) {
   return switch (day) {
      case 1, 2, 3, 4, 5 -> "workday";
      case 6, 7 -> "weekend";
      default -> "invalid";
   };
}

With Java below 12, the same example is pretty more complex.

public String oldMultiSwitch(int day) {
   switch (day) {
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:
         return "workday";
      case 6:
      case 7:
         return "weekend";
      default:
         return "invalid";
   }
}

Sealed Classes (JDK 15)

With the Sealed Classes feature you can restrict the use of a superclass. With a new keyword sealed you may define which other classes or interfaces may extend or implement the current class.

public abstract sealed class Pet permits Cat, Dog {}

A permitted subclass must define a modifier. If you don’t want to allow any other extensions you need to use the final keyword.

public final class Cat extends Pet {}

On the other hand, you may open the class for extensions. In that case, you should use the non-sealed modifier.

public non-sealed class Dog extends Pet {}

Of course, the declaration visible below is NOT ALLOWED.

public final class Tiger extends Pet {}

Text Blocks (JDK 13)

A text block is a multi-line string literal that avoids using escape sequences and automatically formats the string in a predictable way. It also gives the developer control over the format of the string. Since Java 13, Text Blocks are available as a preview feature. They are starting with three double-quote marks ("""). Let’s see how easily we can create and format a JSON message.

public String getNewPrettyPrintJson() {
   return """
          {
             "firstName": "Piotr",
             "lastName": "MiƄkowski"
          }
          """;
}

The same JSON string before Java 13 is pretty more complicated to create.

public String getOldPrettyPrintJson() {
   return "{\n" +
          "     \"firstName\": \"Piotr\",\n" +
          "     \"lastName\": \"MiƄkowski\"\n" +
          "}";
}

New Optional Methods (JDK 9/ JDK 10)

There are several useful methods for Optional since Java 9 and Java 10. The two most interesting of them are orElseThrow and ifPresentOrElse. With the orElseThrow method, you throw NoSuchElementException if no value is present. Otherwise, it returns a value.

public Person getPersonById(Long id) {
   Optional<Person> personOpt = repository.findById(id);
   return personOpt.orElseThrow();
}

Thanks to that, you can avoid using if statement with isPresent method.

public Person getPersonByIdOldWay(Long id) {
   Optional<Person> personOpt = repository.findById(id);
   if (personOpt.isPresent())
      return personOpt.get();
   else
      throw new NoSuchElementException();
}

The second interesting method is ifPresentOrElse. If a value is present, it performs the given action with the value. Otherwise, it performs the given empty-based action.

public void printPersonById(Long id) {
   Optional<Person> personOpt = repository.findById(id);
   personOpt.ifPresentOrElse(
      System.out::println,
      () -> System.out.println("Person not found")
   );
}

With Java 8 we can use if-else directly with the isPresent method.

public void printPersonByIdOldWay(Long id) {
   Optional<Person> personOpt = repository.findById(id);
   if (personOpt.isPresent())
      System.out.println(personOpt.get());
   else
      System.out.println("Person not found");
}

Collection Factory Methods (JDK 9)

With a new feature of Java 9 called Collection Factory Methods, you can easily create immutable collections with predefined data. You just need to use the of method on the particular collection type.

List<String> fruits = List.of("apple", "banana", "orange");
Map<Integer, String> numbers = Map.of(1, "one", 2,"two", 3, "three");

Before Java 9 you might use Collections, but it is definitely a more complex way.

public List<String> fruits() {
   List<String> fruitsTmp = new ArrayList<>();
   fruitsTmp.add("apple");
   fruitsTmp.add("banana");
   fruitsTmp.add("orange");
   return Collections.unmodifiableList(fruitsTmp);
}

public Map<Integer, String> numbers() {
   Map<Integer, String> numbersTmp = new HashMap<>();
   numbersTmp.put(1, "one");
   numbersTmp.put(2, "two");
   numbersTmp.put(3, "three");
   return Collections.unmodifiableMap(numbersTmp);
}

Also, just to create ArrayList from a table of objects, you could use Arrays.asList(...) method.

public List<String> fruitsFromArray() {
   String[] fruitsArray = {"apple", "banana", "orange"};
   return Arrays.asList(fruitsArray);
}

Records (JDK 14)

With Records, you can define immutable, data-only classes (getters only). It automatically creates toString, equals, and hashCode methods. In fact, you just need to define fields as shown below.

public record Person(String name, int age) {}

The class with similar functionality as the record contains fields, constructor, getters, and implementation of toString, equals, and hashCode methods.

public class PersonOld {

    private final String name;
    private final int age;

    public PersonOld(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonOld personOld = (PersonOld) o;
        return age == personOld.age && name.equals(personOld.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "PersonOld{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Private Methods in Interfaces (JDK 9)

Since Java 8, you can have public default methods inside the interface. But only since Java 9, you will be able to take full advantage of this feature thanks to private methods in interfaces.

public interface ExampleInterface {
   private void printMsg(String methodName) {
      System.out.println("Calling interface");
      System.out.println("Interface method: " + methodName);
   }

   default void method1() {
      printMsg("method1");
   }

   default void method2() {
      printMsg("method2");
   }
}

Local Variable Type Inference (JDK 10 / JDK 11)

Since Java 10 you can declare a local variable without its type. You just need to define the var keyword instead of a type. Since Java 11 you can also use it with lambda expressions as shown below.

public String sumOfString() {
   BiFunction<String, String, String> func = (var x, var y) -> x + y;
   return func.apply("abc", "efg");
}

Pattern Matching for switch (JDK 17)

Java 14 has already introduced Pattern Matching for instanceof (JEP-394). Starting from Java 17 you can also use this pattern inside your switch statement. Assuming we use sample classes from the Sealed Classes section we may define such a switch statement:

public String newSwitchWithPatternMatching(Pet pet) {
   return switch (pet) {
      case Cat c -> "cat";
      case Dog d -> "dog";
      default -> "other pet";
   };
}

The post New Developer Friendly Features After Java 8 appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2021/02/01/new-developer-friendly-features-after-java-8/feed/ 3 9400