Transition from Java 8 to Java 11

There's no one-size-fits-all solution to transition code from Java 8 to Java 11. For a non-trivial application, moving from Java 8 to Java 11 can be a significant amount of work. Potential issues include removed API, deprecated packages, use of internal API, changes to class loaders, and changes to garbage collection.

In general, the approaches are to try to run on Java 11 without recompiling, or to compile with JDK 11 first. If the goal is to get an application up and running as quickly as possible, just trying to run on Java 11 is often the best approach. For a library, the goal will be to publish an artifact that is compiled and tested with JDK 11.

Moving to Java 11 is worth the effort. New features have been added and enhancements have been made since Java 8. These features and enhancements improve startup, performance, memory usage, and provide better integration with containers. And there are additions and modifications to API that improve developer productivity.

This document touches on tools to inspect code. It also covers issues that you may run into and recommendations for resolving them. You should also consult other guides, such as the Oracle JDK Migration Guide. How to make existing code modular is not covered here.

The toolbox

Java 11 has two tools, jdeprscan and jdeps, that are useful for sniffing out potential issues. These tools can be run against existing class or jar files. You can assess the transition effort without having to recompile.

jdeprscan looks for use of deprecated or removed API. Use of deprecated API is not a blocking issue, but is something to look into. Is there an updated jar file? Do you need to log an issue to address the use of deprecated API? Use of removed API is a blocking issue that has to be addressed before you try to run on Java 11.

jdeps, which is a Java class dependency analyzer. When used with the --jdk-internals option, jdeps tells you which class depends on which internal API. You can continue to use internal API in Java 11, but replacing the usage should be a priority. The OpenJDK wiki page Java Dependency Analysis Tool has recommended replacements for some commonly used JDK internal APIs.

There are jdeps and jdeprscan plugins for both Gradle and Maven. We recommend adding these tools to your build scripts.

The Java compiler itself, javac, is another tool in your toolbox. The warnings and errors you get from jdeprscan and jdeps will come out of the compiler. The advantage of using jdeprscan and jdeps is that you can run these tools over existing jars and class files, including third-party libraries.

What jdeprscan and jdeps cannot do is warn about the use of reflection to access encapsulated API. Reflective access is checked at runtime. Ultimately, you have to run the code on Java 11 to know with certainty.

Using jdeprscan

The easiest way to use jdeprscan is to give it a jar file from an existing build. You can also give it a directory, such as the compiler output directory, or an individual class name. Use the --release 11 option to get the most complete list of deprecated API. If you want to prioritize which deprecated API to go after, dial the setting back to --release 8. API that was deprecated in Java 8 is likely to be removed sooner than API that has been deprecated more recently.

jdeprscan --release 11 my-application.jar

The jdeprscan tool generates an error message if it has trouble resolving a dependent class. For example, error: cannot find class org/apache/logging/log4j/Logger. Adding dependent classes to the --class-path or using the application class-path is recommended, but the tool will continue the scan without it. The argument is ‑‑class‑path. No other variations of the class-path argument will work.

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

This output tells us that the com.company.Util class is calling a deprecated constructor of the java.lang.Double class. The javadoc will recommend API to use in place of deprecated API. No amount of work will resolve the error: cannot find class sun/misc/BASE64Encoder because it is API that has been removed. Since Java 8, java.util.Base64 should be used.

Run jdeprscan --release 11 --list to get a sense of what API has been deprecated since Java 8. To get a list of API that has been removed, run jdeprscan --release 11 --list --for-removal.

Using jdeps

Use jdeps, with the --jdk-internals option to find dependencies on JDK internal API. The command line option --multi-release 11 is needed for this example because log4j-core-2.13.0.jar is a multi-release jar file. Without this option, jdeps will complain if it finds a multi-release jar file. The option specifies which version of class files to inspect.

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

The output gives some good advice on eliminating use of JDK internal API! Where possible, the replacement API is suggested. The name of the module where the package is encapsulated is given in the parentheses. The module name can be used with --add-exports or --add-opens if it is necessary to explicitly break encapsulation.

The use of sun.misc.BASE64Encoder or sun.misc.BASE64Decoder will result in a java.lang.NoClassDefFoundError in Java 11. Code that uses these APIs has to be modified to use java.util.Base64.

Try to eliminate the use of any API coming from the module jdk.unsupported. API from this module will reference JDK Enhancement Proposal (JEP) 260 as a suggested replacement. In a nutshell, JEP 260 says that the use of internal API will be supported until replacement API is available. Even though your code may use JDK internal API, it will continue to run, for a while at least. Do take a look at JEP 260 since it does point to replacements for some internal API. variable handles can be used in place of some sun.misc.Unsafe API, for example.

jdeps can do more than just scan for use of JDK internals. It is a useful tool for analyzing dependencies and for generating a module-info files. Take a look at the documentation for more.

Using javac

Compiling with JDK 11 will require updates to build scripts, tools, test frameworks, and included libraries. Use the -Xlint:unchecked option for javac to get the details on use of JDK internal API and other warnings. It may also be necessary to use --add-opens or --add-reads to expose encapsulated packages to the compiler (see JEP 261).

Libraries can consider packaging as a multi-release jar file. Multi-release jar files allow you to support both Java 8 and Java 11 runtimes from the same jar file. They do add complexity to the build. How to build multi-release jars is beyond the scope of this document.

Running on Java 11

Most applications should run on Java 11 without modification. The first thing to try is to run on Java 11 without recompiling the code. The point of just running is to see what warnings and errors come out of the execution. This approach gets an
application to run on Java 11 more quickly by focusing on the minimum that needs to be done.

Most of the problems you may encounter can be resolved without having to recompile code. If an issue has to be fixed in the code, then make the fix but continue to compile with JDK 8. If possible, work on getting the application to run with java version 11 before compiling with JDK 11.

Check command line options

Before running on Java 11, do a quick scan of the command-line options. Options that have been removed will cause the Java Virtual Machine (JVM) to exit. This check is especially important if you use GC logging options since they have changed drastically from Java 8. The JaCoLine tool is a good one to use to detect problems with the command line options.

Check third-party libraries

A potential source of trouble is third-party libraries that you don't control. You can proactively update third-party libraries to more recent versions. Or you can see what falls out of running the application and only update those libraries that are necessary. The problem with updating all libraries to a recent version is that it makes it harder to find root cause if there is some error in the application. Did the error happen because of some updated library? Or was the error caused by some change in the runtime? The problem with updating only what's necessary is that it may take several iterations to resolve.

The recommendation here is to make as few changes as possible and to update third-party libraries as a separate effort. If you do update a third-party library, more often than not you will want the latest-and-greatest version that is compatible with Java 11. Depending on how far behind your current version is, you may want to take a more cautious approach and upgrade to the first Java 9+ compatible version.

In addition to looking at release notes, you can use jdeps and jdeprscan to assess the jar file. Also, the OpenJDK Quality Group maintains a Quality Outreach wiki page that lists the status of testing of many Free Open Source Software (FOSS) projects against versions of OpenJDK.

Explicitly set garbage collection

The Parallel garbage collector (Parallel GC) is the default GC in Java 8. If the application is using the default, then the GC should be explicitly set with the command-line option -XX:+UseParallelGC. The default changed in Java 9 to the Garbage First garbage collector (G1GC). In order to make a fair comparison of an application running on Java 8 versus Java 11, the GC settings must be the same. Experimenting with the GC settings should be deferred until the application has been validated on Java 11.

Explicitly set default options

If running on the HotSpot VM, setting the command line option -XX:+PrintCommandLineFlags will dump the values of options set by the VM, particularly the defaults set by the GC. Run with this flag on Java 8 and use the printed options when running on Java 11. For the most part, the defaults are the same from 8 to 11. But using the settings from 8 ensures parity.

Setting the command line option --illegal-access=warn is recommended. In Java 11, using reflection to access to JDK-internal API will result in an illegal reflective access warning. By default, the warning is only issued for the first illegal access. Setting --illegal-access=warn will cause a warning on every illegal reflective access. You will find more case if illegal access with the option set to warn. But you will also get a lot of redundant warnings.
Once the application runs on Java 11, set --illegal-access=deny to mimic the future behavior of the Java runtime. Starting with Java 16, the default will be --illegal-access=deny.

ClassLoader cautions

In Java 8, you can cast the system class loader to a URLClassLoader. This is usually done by applications and libraries that want to inject classes into the classpath at runtime. The class loader hierarchy has changed in Java 11. The system class loader (also known as the application class loader) is now an internal class. Casting to a URLClassLoader will throw a ClassCastException at runtime. Java 11 does not have API to dynamically augment the classpath at runtime but it can be done through reflection, with the obvious caveats about using internal API.

In Java 11, the boot class loader only loads core modules. If you create a class loader with a null parent, it may not find all platform classes. In Java 11, you need to pass ClassLoader.getPlatformClassLoader() instead of null as the parent class loader in such cases.

Locale data changes

The default source for locale data in Java 11 was changed with JEP 252 to the Unicode Consortium's Common Locale Data Repository. This may have an impact on localized formatting. Set the system property java.locale.providers=COMPAT,SPI to revert to the Java 8 locale behavior, if necessary.

Potential issues

Here are some of the common issues you might come across. Follow the links for more details about these issues.

Unrecognized options

If a command-line option has been removed, the application will print Unrecognized option: or Unrecognized VM option followed by the name of the offending option. An unrecognized option will cause the VM to exit. Options that have been deprecated, but not removed, will produce a VM warning.

In general, options that were removed have no replacement and the only recourse is to remove the option from the command line. The exception is options for garbage collection logging. GC logging was reimplemented in Java 9 to use the unified JVM logging framework. Refer to "Table 2-2 Mapping Legacy Garbage Collection Logging Flags to the Xlog Configuration" in the section Enable Logging with the JVM Unified Logging Framework of the Java SE 11 Tools Reference.

VM warnings

Use of deprecated options will produce a warning. An option is deprecated when it has been replaced or is no longer useful. As with removed options, these options should be removed from the command line. The warning VM Warning: Option <option> was deprecated means that the option is still supported, but that support may be removed in the future. An option that is no longer supported and will generate the warning VM Warning: Ignoring option. Options that are no longer supported have no effect on the runtime.

The web page VM Options Explorer provides an exhaustive list of options that have been added to or removed from Java since JDK 7.

Error: Could not create the Java Virtual Machine

This error message is printed when the JVM encounters an unrecognized option.

WARNING: An illegal reflective access operation has occurred

When Java code uses reflection to access JDK-internal API, the runtime will issue an illegal reflective access warning.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

What this means is that a module has not exported the package that is being accessed through reflection. The package is encapsulated in the module and is, basically, internal API. The warning can be ignored as a first effort to getting up and running on Java 11. The Java 11 runtime permits the reflective access so that legacy code can continue to work.

To address this warning, look for updated code that does not make use of the internal API. If the issue cannot be resolved with updated code, either the --add-exports or the --add-opens command-line option can be used to open access to the package. These options allow access to unexported types of one module from another module.

The --add-exports option allows the target module to access the public types of the named package of the source module. Sometimes code will use setAccessible(true) to access non-public members and API. This is known as deep reflection. In this case, use --add-opens to give your code access to the non-public members of a package. If you are unsure whether to use --add-exports or --add-opens, start with --add-exports.

The --add-exports or --add-opens options should be considered as a work-around, not a long-term solution. Using these options breaks encapsulation of the module system, which is meant to keep JDK-internal API from being used. If the internal API is removed or changes, the application will fail. Reflective access will be denied in Java 16, except where access enabled by command line options such as --add-opens. To mimic the future behavior, set --illegal-access=deny on the command line.

The warning in the example above is issued because the sun.nio.ch package is not exported by the java.base module. In other words, there is no exports sun.nio.ch; in the module-info.java file of module java.base. This can be resolved with --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. Classes that are not defined in a module implicitly belong to the unnamed module, literally named ALL-UNNAMED.

java.lang.reflect.InaccessibleObjectException

This exception indicates that you are trying to call setAccessible(true) on a field or method of an encapsulated class. You may also get an illegal reflective access warning. Use the --add-opens option to give your code access to the non-public members of a package. The exception message will tell you the module "does not open" the package to the module that is trying to call setAccessible. If the module is "unnamed module", use UNNAMED-MODULE as the target-module in the --add-opens option.

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

NoClassDefFoundError is most likely caused by a split package, or by referencing removed modules.

NoClassDefFoundError caused by split-packages

A split package is when a package is found in more than one library. The symptom of a split-package problem is that a class you know to be on the class-path is not found.

This issue will only occur when using the module-path. The Java module system optimizes class lookup by restricting a package to one named module. The runtime gives preference to the module-path over the class-path when doing a class lookup. If a package is split between a module and the class-path, only the module is used to do the class lookup. This can lead to NoClassDefFound errors.

An easy way to check for a split package is to plug your module path and class path into jdeps and use the path to your application class files as the <path>. If there is a split package, jdeps will print out a warning: Warning: split package: <package-name> <module-path> <split-path>.

This issue can be resolved by using --patch-module <module-name>=<path>[,<path>] to add the split package into the named module.

NoClassDefFoundError caused by using Java EE or CORBA modules

If the application runs on Java 8 but throws a java.lang.NoClassDefFoundError or a java.lang.ClassNotFoundException, then it is likely that the application is using a package from the Java EE or CORBA modules. These modules were deprecated in Java 9 and removed in Java 11.

To resolve the issue, add a runtime dependency to your project.

Removed module Affected Package Suggested dependency
Java API for XML Web Services (JAX-WS) java.xml.ws JAX WS RI Runtime
Java Architecture for XML Binding (JAXB) java.xml.bind JAXB Runtime
JavaBeans Activation Framework (JAV) java.activation JavaBeans (TM) Activation Framework
Common Annotations java.xml.ws.annotation Javax Annotation API
Common Object Request Broker Architecture (CORBA) java.corba GlassFish CORBA ORB
Java Transaction API (JTA) java.transaction Java Transaction API

-Xbootclasspath/p is no longer a supported option

Support for -Xbootclasspath/p has been removed. Use --patch-module instead. The --patch-module option is described in JEP 261. Look for the section labeled "Patching module content". --patch-module can be used with javac and with java to override or augment the classes in a module.

What --patch-module does, in effect, is insert the patch module into the module system's class lookup. The module system will grab the class from the patch module first. This is the same effect as pre-pending the bootclasspath in Java 8.

UnsupportedClassVersionError

This exception means that you are trying to run code that was compiled with a later version of Java on an earlier version of Java. For example, you are running on Java 11 with a jar that was compiled with JDK 13.

Java version Class file format version
8 52
9 53
10 54
11 55
12 56
13 57

Next steps

Once the application runs on Java 11, consider moving libraries off the class-path and onto the module-path. Look for updated versions of the libraries your application depends on. Choose modular libraries, if available. Use the module-path as much as possible, even if you don't plan on using modules in your application. Using the module-path has better performance for class loading than the class-path does.