Life after Java 8 is in full swing, with fast microcontainers, cloud-native deployment, and a more performant JVM. And although BellSoft supports JDK 8 until March 2031, there will come a day when you have to let Java 8 go. Besides, with a new 2-year LTS-release cadence, Java gets new features, and APIs are updated more often, making the migration process more challenging regarding compatibility.
We understand that upgrading the Java version in a complex enterprise project is not a one-click task, yet, it is not mission impossible. This article will guide you through the changes you need to introduce related to
- Framework and dependencies versions,
- Deprecated, updated, or added features,
- Garbage collection amendments,
- New APIs,
- And highlight the most common issues that you may encounter after the migration.
And by the way, there’s a one-click solution to the issue — read on to find out!
Table of Contents
Key changes and enhancements up to JDK 17
- Modularity. Starting with Java 9, Java applications are organized as modules, i.e., groups of closely related packages and resources (are now shipped with a module that needs them instead of being put into the root) with a module descriptor file. Modularity makes Java applications more manageable and enables developers to create custom JRE images with only those modules that are required for running their applications, thus reducing the size of a final container image significantly. Note that Java 11 supports both module- and classpath-based configuration, but as you migrate to Java 17, you must modularize your application.
- Comprehensive support for containers. Ever since JDK 9, Java has become increasingly container aware. Some notable changes include but are not limited to:
- Support for cgroup memory limits in container environments,
- Improved Docker container detection and resource configuration usage,
- Flexible heap percentage selection of available RAM,
- Updated CPU count algorithm,
- Container metrics,
- Cgroups v2: Container awareness,
- Alpine Linux port for OpenJDK.
A few of these improvements were ported to JDK 8, but in general, newer Java versions feel much better in containers and enable developers to use cloud resources more efficiently.
- Garbage collection changes:
- New GC implementations — Z GC (scalable low-latency collector) and Shenandoah GC (a low pause collector effective even with large heaps). In addition, G1 GC is the default collector starting with Java 9.
- Removed CMS GC,
- New GC logs aligned with the improved JVM logging system (see below).
- JDK Flight Recorder and JDK Mission Control. JFR and JMC, previously commercial features in Oracle Java, were released into open source and are now available for free with OpenJDK distributions.
- Java Virtual Machine Tool Interface (JVMTI). The addition of a native programming interface supporting various tools for JVM debugging, monitoring, and profiling enables the developers to inspect and control the state of JVM processes more efficiently.
- New JVM logging system. A new logging system introduced in JDK 9 allows for unified command line options for all logging, classification of log messages by tags, dynamic logging at runtime using jcmd or MBeans, and so on.
- JShell. The Java Shell tool is an interactive REPL (Read Evaluate Print Loop) tool for learning the Java language, exploring new APIs, and prototyping new code. It evaluates statements, expressions, and declarations as they are entered and immediately shows the results.
- New HTTP/2 API that allows for more flexible handling of HTTP requests.
- Strongly encapsulated JDK internals. The goal of forbidding access to JDK internal components is to encourage developers to use standard APIs for easier upgrading in the future and increase overall code security. Developers who use internal components will have to rewrite their code significantly as these packages now belong to modules unavailable for public access.
JDK 17 also enables the developers to take advantage of GraalVM. GraalVM is a platform that provides an enhanced JIT compiler, AOT compiler, and tools for running multilingual projects. Experimental support for AOT features and Graal JIT was added to JDK 9 and removed from JDK 16. GraalVM now actively evolves as a separate project. Builds are available for JDK 11 (GraalVM CE only), 17, and the latest Java release.
In addition, many components were deprecated, amended, or eliminated since Java 8, so we recommend checking the full list of removed tools and components and security updates to avoid issues with compiling the application on a newer Java version.
How to migrate the enterprise Java project from Java 8
Update Java version, dependencies, and tools
First thing first, download and install the latest update of JDK 17. If you are weary of constant Oracle’s Java licensing changes and soaring prices, we recommend using Liberica JDK, a free and 100% open-source Java runtime recommended by Spring with
- The widest range of supported system configurations,
- Three flavors for different needs: Standard, Full (with JavaFX), and Lite (optimized for cloud),
- The smallest containers,
- Affordable commercial support with 24/7 service and emergency patches/fixes based on strict SLA.
The next step is to update all third-party libraries, build tools, IDEs, and a framework with associated dependencies to the latest version that supports JDK 17. For instance, the minimal Spring Boot version required for Java 17 is 2.5. But consider upgrading to Spring Boot 3.x, the first major release in 4.5 years with 44 new features, 100+ enhancements, and baked-in support for GraalVM Native Image.
Indeed, updating all dependencies is easier said than done, but otherwise, your application will fail to start.
If at some point after the updates you get an UnsupportedClassVersionError
, it means you use the code compiled for the newer Java version, 18–20.
After updating the project components, compile your application. Resolve the remaining issues based on the errors and warnings you see (some of them we address below).
If the program starts successfully, run the tests to verify its behavior: you will likely see more inconsistencies (for instance, different locale data formatting due to changes introduced by JEP 252).
Modularize your application
To organize an application into modules, use module declarations — special module-info.java files put into the project’s root. They contain the data required to build a module (name, dependencies, public packages, reflection permissions, services offered, and services consumed). When working with Java modules, keep in mind that:
- Module names shouldn’t conflict, so they must be unique. It is recommended to use the reverse-domain-name pattern to name modules.
- All the packages are private by default, so you need to explicitly specify the public packages you want to make accessible to the outer world. The same rule applies to reflection.
- Only modules with added exports clause make the public types in certain packages available for use by other modules. In other words, module B is readable when module A depends upon module B and exports it. In this case, public types of module B are considered accessible by the JVM. All the other modules are treated as internal and cannot be accessed from within.
- Split packages (two packages with the same name belonging to different libraries) are not allowed, meaning several modules cannot export the same package simultaneously.
- Circular dependencies (when two or more modules depend on each other to function) are also not allowed.
Update Docker images
If you use Docker container images in production, update your Dockerfile as well.
BellSoft provides a broad selection of Liberica JDK and JRE Docker images for the most popular Linux distributions, including Alpaquita Linux, a minimalistic 100% Alpine-compatible distro with two libc implementations, enhanced security and performance, and LTS releases. Together with Liberica JRE Lite, it enables the developers to build performant microcontainers.
Change this line in the Dockerfile to move your app into a lightweight container with Liberica and Alpaquita:
FROM bellsoft/liberica-runtime-container:jre-17-stream-musl
Alternatively, follow this guide to compile your application in a container with Liberica JDK and shift it into another container with Liberica JRE (we strongly recommend utilizing JRE builds for deployment to minimize resource consumption).
Add missing dependencies for removed Java APIs and modules
If you encounter a java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException when running your application on Java 17, you probably use modules that no longer exist in the JDK. For instance, the following Java EE modules were removed from Java 11:
- xml.ws (JAX-WS, plus the related technologies SAAJ and Web Services Metadata),
- xml.bind (JAXB),
- activation (JAF),
- xml.ws.annotation (Common Annotations),
- corba (CORBA),
- transaction (JTA).
These modules are maintained separately, so you should add them as dependencies to your project to resolve compilation issues.
In addition, the JavaFX and Web Start technologies were deleted from JDK 11 as well. But you can use Liberica Full with LibericaFX, an open-source implementation of JavaFX. Builds with OpenWebStart (reimplementation of Web Start) are also available.
Overall, numerous APIs were taken out of JDK up to version 17. To save time on manual tracing, use the jdeprscan
tool, which scans a jar file for deprecated or removed APIs. jdeprscan
identifies only Java SE APIs and won’t work with third-party libraries.
You can give jdeprscan
a jar file, a directory, or a separate class name. If the tool produces a message error: cannot find class …
, adjust the classpath to include all dependent classes. But it is also possible that the file uses a removed API.
For instance, let’s scan an aspectjweaver-1.9.9.1.jar:
jdeprscan aspectjweaver-1.9.9.1.jar
The output is:
Jar file aspectjweaver-1.9.9.1.jar:
class org/aspectj/weaver/bcel/ExtensibleURLClassLoader uses deprecated method java/lang/ClassLoader::getPackage(Ljava/lang/String;)Ljava/lang/Package;
class org/aspectj/lang/Aspects14 uses deprecated method java/lang/reflect/AccessibleObject::isAccessible()Z
error: cannot find class org/apache/commons/logging/LogFactory
class org/aspectj/lang/Aspects uses deprecated method java/lang/reflect/AccessibleObject::isAccessible()Z
error: cannot find class org/apache/commons/logging/Log
class org/aspectj/weaver/loadtime/definition/DocumentParser uses deprecated class org/xml/sax/helpers/XMLReaderFactory
class org/aspectj/util/FileUtil uses deprecated method java/io/DataInputStream::readLine()Ljava/lang/String;
class org/aspectj/util/FileUtil uses deprecated method java/io/File::toURL()Ljava/net/URL;
You can also use a --for-removal
flag to limit the scan to APIs deprecated for removal. A --list
parameter lists all deprecated APIs without any scanning.
Fix access to internal APIs
Strong encapsulation introduced in Java 17 forbids the usage of reflection to access internal JDK components. If your code accesses non-public APIs, the application will throw an InaccessibleObjectException
. JDK versions 9–16 allowed reflective access to JDK internal with the --illegal-access
option, but as most libraries now use standard APIs, the option is no longer applicable in Java 17.
To solve this problem, use the jdeps
tool, a dependency analyzer. When used with a --jdk-internals
option, it lists class-level dependencies in the JDK internal APIs. You can update the tools and libraries in question based on this information.
If, for some reason, you can’t replace some libraries with newer versions, two options provide access to internal APIs for these components:
--add-exports
to access a strongly encapsulated internal API,--add-opens
to access non-public fields and methods by reflection.
But remember that accessing the internal JDK APIs is unsafe, and while you can use these options for some time as an emergency, we strongly recommend rewriting the code.
Check for deprecated and removed JVM parameters
Some JVM parameters were deprecated or removed from the JDK. If your application uses removed parameters, it will print Unrecognized VM option option-name
, and the JVM will exit with Error: Could not create the Java Virtual Machine
. A warning will be issued in the case of obsolete or deprecated options.
The only way of dealing with unrecognized parameters is to remove them.
Convert legacy logging flags to a new system
GC logging was reimplemented in Java 9 with JEP 271 to align with the unified JVM logging framework. As a result, legacy GC flags won’t be accepted. A guide to mapping legacy garbage collection logging flags to the Xlog configuration can be found in the official documentation’s Convert GC Logging Flags to Xlog section. The document also contains a guide to mapping legacy runtime logging flags to the new configuration.
Feeling overwhelmed? BellSoft’s solution to Java migration problem
If, after reading this guide, you feel that upgrading the Java version at your company will take much longer than you expected, or you aren’t ready to burden your engineers with migration due to some more urgent tasks, don’t worry — there is a way to stay on Java 8 and still take advantage of Java 17 features and performance!
BellSoft prepared a solution that bring the power of JVM 17 to your workloads running on Java 8 — Liberica JDK Performance Edition 8. It couples JVM 17 and JDK 8 and helps enterprises to boost the performance of theor applications bz up to 10% immediatelz without changing the version of Java, framework, or libraries. No incompatibility issues, significant code changes, or stress!