jdk Archives - Piotr's TechBlog https://piotrminkowski.com/tag/jdk/ Java, Spring, Kotlin, microservices, Kubernetes, containers Fri, 17 Feb 2023 13:53:23 +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 jdk Archives - Piotr's TechBlog https://piotrminkowski.com/tag/jdk/ 32 32 181738725 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