Java 21 vs. Java 11 - practical comparison
Introduction
This study aims to compare some metrics related to Java 11 and Java 21 applications and based on it, approximate the value of savings due to migration.
In the tests I used following JDKs:
- for Java 11: Coretto openjdk 11.0.19 2023-04-18 LTS
- for Java 21: Oracle openjdk 21.0.1 2023-10-17
Hardware:
- model: Lenovo Y50-70
- RAM: 16GB DDR3 1600MHz
- CPU: Intel Core i7-4720HQ (Quad Core, 2.6GHz - 3.6GHz, 6MB Cache)
- OS: Ubuntu Desktop 22.04.3 LTS
Tested aspects
Each test was done 10 times, one by one. Collected values description:
- application start time – logged in application output:
- application initial memory usage – RAM memory usage after application started (by PID from app logs):
- application memory usage after init – RAM memory usage after sending request to application endpoint (by PID from app logs):
Scenarios
I defined following testing scenarios:
- JDK11 jar - running Java 11 jar file directly from terminal with JDK11,
- JDK21 11 jar - running Java 11 jar file directly from terminal with JDK21,
- JDK 21 21 jar - running Java 21 optimized jar file directly from terminal with JDK21,
- IJ 11 jar - running Java 11 jar file from IntelliJ with JDK11,
- IJ 21 11 jar - running Java 11 jar file from IntelliJ with JDK21,
- IJ 21 21 jar - running Java 21 optimized jar file from IntelliJ with JDK21,
- docker images size,
- docker containers RAM usage.
Commands used for running app
Direct run command:
java -jar app.jar
Command created by IntelliJ:
~/.jdks/corretto-11.0.19/bin/java -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:/snap/intellij-idea-ultimate/456/lib/idea_rt.jar=33137:/snap/intellij-idea-ultimate/456/bin -Dfile.encoding=UTF-8 -classpath ~/my_programming/todo-app/target/classes:~/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.7.13/spring-boot-starter-web-2.7.13.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter/2.7.13/spring-boot-starter-2.7.13.jar:~/.m2/repository/org/springframework/boot/spring-boot/2.7.13/spring-boot-2.7.13.jar:~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.7.13/spring-boot-autoconfigure-2.7.13.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.7.13/spring-boot-starter-logging-2.7.13.jar:~/.m2/repository/ch/qos/logback/logback-classic/1.2.12/logback-classic-1.2.12.jar:~/.m2/repository/ch/qos/logback/logback-core/1.2.12/logback-core-1.2.12.jar:~/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.17.2/log4j-to-slf4j-2.17.2.jar:~/.m2/repository/org/apache/logging/log4j/log4j-api/2.17.2/log4j-api-2.17.2.jar:~/.m2/repository/org/slf4j/jul-to-slf4j/1.7.36/jul-to-slf4j-1.7.36.jar:~/.m2/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:~/.m2/repository/org/yaml/snakeyaml/1.30/snakeyaml-1.30.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.7.13/spring-boot-starter-json-2.7.13.jar:~/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.13.5/jackson-datatype-jdk8-2.13.5.jar:~/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.13.5/jackson-module-parameter-names-2.13.5.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.7.13/spring-boot-starter-tomcat-2.7.13.jar:~/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.76/tomcat-embed-core-9.0.76.jar:~/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/9.0.76/tomcat-embed-el-9.0.76.jar:~/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.76/tomcat-embed-websocket-9.0.76.jar:~/.m2/repository/org/springframework/spring-web/5.3.28/spring-web-5.3.28.jar:~/.m2/repository/org/springframework/spring-beans/5.3.28/spring-beans-5.3.28.jar:~/.m2/repository/org/springframework/spring-webmvc/5.3.28/spring-webmvc-5.3.28.jar:~/.m2/repository/org/springframework/spring-context/5.3.28/spring-context-5.3.28.jar:~/.m2/repository/org/springframework/spring-expression/5.3.28/spring-expression-5.3.28.jar:~/.m2/repository/org/projectlombok/lombok/1.18.28/lombok-1.18.28.jar:~/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar:~/.m2/repository/jakarta/xml/bind/jakarta.xml.bind-api/2.3.3/jakarta.xml.bind-api-2.3.3.jar:~/.m2/repository/jakarta/activation/jakarta.activation-api/1.2.2/jakarta.activation-api-1.2.2.jar:~/.m2/repository/net/bytebuddy/byte-buddy/1.12.23/byte-buddy-1.12.23.jar:~/.m2/repository/org/springframework/spring-core/5.3.28/spring-core-5.3.28.jar:~/.m2/repository/org/springframework/spring-jcl/5.3.28/spring-jcl-5.3.28.jar:~/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.13.5/jackson-datatype-jsr310-2.13.5.jar:~/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.13.5/jackson-annotations-2.13.5.jar:~/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.13.5/jackson-core-2.13.5.jar:~/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.13.5/jackson-databind-2.13.5.jar:~/.m2/repository/org/postgresql/postgresql/42.3.8/postgresql-42.3.8.jar:~/.m2/repository/org/checkerframework/checker-qual/3.5.0/checker-qual-3.5.0.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-data-jpa/2.7.13/spring-boot-starter-data-jpa-2.7.13.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-aop/2.7.13/spring-boot-starter-aop-2.7.13.jar:~/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-jdbc/2.7.13/spring-boot-starter-jdbc-2.7.13.jar:~/.m2/repository/com/zaxxer/HikariCP/4.0.3/HikariCP-4.0.3.jar:~/.m2/repository/org/springframework/spring-jdbc/5.3.28/spring-jdbc-5.3.28.jar:~/.m2/repository/jakarta/transaction/jakarta.transaction-api/1.3.3/jakarta.transaction-api-1.3.3.jar:~/.m2/repository/jakarta/persistence/jakarta.persistence-api/2.2.3/jakarta.persistence-api-2.2.3.jar:~/.m2/repository/org/hibernate/hibernate-core/5.6.15.Final/hibernate-core-5.6.15.Final.jar:~/.m2/repository/antlr/antlr/2.7.7/antlr-2.7.7.jar:~/.m2/repository/org/jboss/jandex/2.4.2.Final/jandex-2.4.2.Final.jar:~/.m2/repository/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar:~/.m2/repository/org/hibernate/common/hibernate-commons-annotations/5.1.2.Final/hibernate-commons-annotations-5.1.2.Final.jar:~/.m2/repository/org/glassfish/jaxb/jaxb-runtime/2.3.8/jaxb-runtime-2.3.8.jar:~/.m2/repository/org/glassfish/jaxb/txw2/2.3.8/txw2-2.3.8.jar:~/.m2/repository/com/sun/istack/istack-commons-runtime/3.0.12/istack-commons-runtime-3.0.12.jar:~/.m2/repository/com/sun/activation/jakarta.activation/1.2.2/jakarta.activation-1.2.2.jar:~/.m2/repository/org/springframework/data/spring-data-jpa/2.7.13/spring-data-jpa-2.7.13.jar:~/.m2/repository/org/springframework/data/spring-data-commons/2.7.13/spring-data-commons-2.7.13.jar:~/.m2/repository/org/springframework/spring-orm/5.3.28/spring-orm-5.3.28.jar:~/.m2/repository/org/springframework/spring-tx/5.3.28/spring-tx-5.3.28.jar:~/.m2/repository/org/springframework/spring-aspects/5.3.28/spring-aspects-5.3.28.jar:~/.m2/repository/org/keycloak/keycloak-spring-boot-starter/21.1.1/keycloak-spring-boot-starter-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-spring-boot-2-adapter/21.1.1/keycloak-spring-boot-2-adapter-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-authz-client/21.1.1/keycloak-authz-client-21.1.1.jar:~/.m2/repository/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar:~/.m2/repository/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar:~/.m2/repository/commons-codec/commons-codec/1.15/commons-codec-1.15.jar:~/.m2/repository/org/keycloak/spring-boot-container-bundle/21.1.1/spring-boot-container-bundle-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-adapter-core/21.1.1/keycloak-adapter-core-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-crypto-default/21.1.1/keycloak-crypto-default-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-server-spi/21.1.1/keycloak-server-spi-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-server-spi-private/21.1.1/keycloak-server-spi-private-21.1.1.jar:~/.m2/repository/org/bouncycastle/bcpkix-jdk15on/1.70/bcpkix-jdk15on-1.70.jar:~/.m2/repository/org/bouncycastle/bcutil-jdk15on/1.70/bcutil-jdk15on-1.70.jar:~/.m2/repository/org/keycloak/keycloak-spring-security-adapter/21.1.1/keycloak-spring-security-adapter-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-adapter-spi/21.1.1/keycloak-adapter-spi-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-policy-enforcer/21.1.1/keycloak-policy-enforcer-21.1.1.jar:~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.70/bcprov-jdk15on-1.70.jar:~/.m2/repository/org/keycloak/keycloak-spring-boot-adapter-core/21.1.1/keycloak-spring-boot-adapter-core-21.1.1.jar:~/.m2/repository/org/jboss/logging/jboss-logging/3.4.3.Final/jboss-logging-3.4.3.Final.jar:~/.m2/repository/org/keycloak/keycloak-core/21.1.1/keycloak-core-21.1.1.jar:~/.m2/repository/org/keycloak/keycloak-common/21.1.1/keycloak-common-21.1.1.jar:~/.m2/repository/org/springframework/boot/spring-boot-starter-security/2.7.13/spring-boot-starter-security-2.7.13.jar:~/.m2/repository/org/springframework/spring-aop/5.3.28/spring-aop-5.3.28.jar:~/.m2/repository/org/springframework/security/spring-security-config/5.7.9/spring-security-config-5.7.9.jar:~/.m2/repository/org/springframework/security/spring-security-core/5.7.9/spring-security-core-5.7.9.jar:~/.m2/repository/org/springframework/security/spring-security-crypto/5.7.9/spring-security-crypto-5.7.9.jar:~/.m2/repository/org/springframework/security/spring-security-web/5.7.9/spring-security-web-5.7.9.jar com.example.todoapp.TodoAppApplication
Results
Test results (simplified to averages etc) are presented in table below:
I have also tested differences in docker images size (output below) and docker container RAM usage (using docker stats: 489MB for Java 11 vs 292MB for Java 21).
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE todo-app-jdk21 latest 96ff1c52ad7f 3 seconds ago 552MB todo-app-jdk11 latest eb2b4728d2f8 About a minute ago 705MB
Conclusion
Java 21 app seems to be:
- better optimized - RAM usage is significantly lower (over 100MB difference for directly running jar and about 200MB for docker image), also initial request response was faster (from 800-1000 ms to 350-450 ms), output jar file is smaller (51.3 MB vs 47.8 MB) and also docker image (705MB vs 552MB),
- more stable - RAM usage is similar every re-run.
In microservices world we can have hundreds or thousands running instances of our apps.
Let's do some math: i.e. 200MB * 1000 instances = 200GB extra RAM required.
It's fixed cost paid just for being a fan of Java 11.
Summary
Java 21 have many more features which I didn't test here, like concurency and security improvements or new ways of writing clean code.
Why to migrate? (for developer)
- new possibilities of writing clean code,
- performance improvements,
- security level upgrade,
- lower probability of "weird bugs".
Why to migrate? (for user)
- user data is better secured,
- higher system awailability due to better app performance,
- less timeouts.
Why to migrate? (for manager)
- increasing the level of security,
- optimizing costs of developing and maintaining system,
- reducing conflicts probability,
- better well-being of employees in the work environment.