Although the latest version of Java has evolved to Java 18, most projects on the market are still using Java 8. Many people are apprehensive about upgrading their Java version because the Java API is not necessarily forward compatible from Java 8. Java 11 is the next long-supported version of Java 8, and there is no doubt that Java 11 is even better than Java 8. superior.

This article describes the code checking tools used to convert code from Java 8 to Java 11, as well as the problems you may encounter and suggestions for resolving them.

Why Java 11 is needed

Java 11 is the next long-term supported version of Java 8, which means that Java 8 is no longer officially supported. In addition, the Java platform has changed significantly from Java 8 to Java 11, and these changes have been made to make the Java platform even better.

This article focuses on the changes that had an impact on performance, diagnostics, and productivity.


Modules address configuration and packaging issues that are difficult to manage in large applications (running on classpath). A module is a self-descriptive collection of Java classes and interfaces and related resources.

With modules, it is possible to customize runtime configurations that contain only the components required by the application. This customization produces a small memory footprint, so the application can be statically linked to the custom runtime for deployment using jlink. This smaller memory footprint may be particularly suitable for microservice architectures.

Internally, the JVM can make use of modules by making class loading more efficient. The result is a smaller, lighter and faster start-up runtime. The optimization techniques used by the JVM to improve application performance can be more effective because modules can code what components are needed for a particular class.

For programmers, modules can help enforce strong encapsulation by requiring explicit declarations of which packages a module can export and which components it requires, and by restricting reflective access. This level of encapsulation makes the application more secure and easier to maintain.

Applications can continue to use classpath and run on Java 11 without converting to modules that are required components.

Java Network Traffic Recorder

Java Flight Recorder (JFR) collects diagnostic and analytical data from running Java applications. JFR has little to no effect on running Java applications. The collected data can then be analyzed using Java Mission Control (JMC) and other tools. While both JFR and JMC are commercial features in Java 8, both are open source in Java 11.

Java Mission Control

java Mission Control (JMC) provides a graphical display of the data collected by the java network traffic logger (JFR) and is open source in java 11. In addition to general information about the running application, JMC allows the user to drill down into the data. JFR and JMC can be used to diagnose runtime problems such as memory leaks, GC overhead, hot methods, thread bottlenecks, and blocking I/O.

Unified Logging

Java 11 has a common logging system for all components of the JVM. Users can use this unified logging system to define which components need to be logged, and to what level. This granular logging is useful for root cause analysis of JVM crashes and for diagnosing performance issues in production environments.

Low Overhead Heap Analysis

A new API has been added to the Java Virtual Machine Tools Interface (JVMTI) for sampling Java heap allocations. Sampling is low overhead and can be enabled on an ongoing basis. While heap allocation can be monitored using the Java Flight Recorder (JFR), the sampling methods in JFR can only be used for allocation. JFR implementations may also fail to hit allocations. In contrast, heap sampling in Java 11 can provide information about both live and dead objects.

Application Performance Monitoring (APM) vendors are starting to take advantage of this new feature, and the Java engineering group is investigating the possibility of using it with Azure performance monitoring tools.


When logging, you typically get a snapshot of the current thread’s stack. The question is how many stack traces to log, and whether it is necessary to log stack traces. For example, a user may only want to see the stack trace when a specific exception occurs in a method. The StackWalker class (added in Java 9) provides a snapshot of the stack and provides ways to facilitate fine-grained programmer control over how the stack trace is used.

Garbage collection

Java 11 provides the following garbage collectors: Serial, Parallel, Garbage-First, and Epsilon. The default garbage collector in Java 11 is the Garbage First garbage collector (G1GC).

The other three collectors are mentioned here to keep the content complete. The Z Garbage Collector (ZGC) is a concurrent, low-latency collector that tries to keep the pause time below 10 milliseconds. ZGC is available as an experimental feature in Java 11. The Shenandoah recycler is a short-pause recycler that performs more garbage collection in a concurrent manner through running Java programs, thus reducing GC pause time. Shenandoah was an experimental feature in Java 12, but can be backward ported to Java 11. The Concurrent Mark and Sweep (CMS) recycler was released, but has been deprecated since the release of Java 9.

For general use, the JVM uses GC as the default setting. Typically, these and other GC settings need to be adjusted to optimize throughput or latency according to the requirements of the application. Proper GC tuning requires a deep understanding of GC and requires expertise from the Microsoft Java Engineering Group.


The default garbage collector in Java 11 is the G1 Garbage Collector (G1GC). The goal of G1GC is to strike a balance between latency and throughput. The G1 garbage collector attempts to achieve a high throughput goal with a high probability of meeting the pause time goal. G1GC aims to avoid entire collections, but fallback to full GC occurs when concurrent recovery cannot recover memory fast enough. Full GC uses the same number of parallel worker threads as the initial mixed recovery.

Parallel GC

The parallel recycler is the default recycler in Java 8. Parallel GC is a throughput recycler that uses multiple threads to speed up garbage collection.


The Epsilon garbage collector handles allocations, but does not reclaim any memory. When the heap is exhausted, the JVM shuts down. Epsilon is suitable for services with short lifetimes and applications that are known to have no garbage.

Docker container improvements

Prior to Java 10, the JVM did not recognize memory and CPU constraints set on the container. For example, in Java 8, the JVM would set the maximum heap size to a quarter of the physical memory of the base host by default. Starting with Java 10, the JVM uses the constraints set by container control groups (cgroups) to set memory and CPU limits (see the description below). For example, the default maximum heap size is a quarter of the container’s memory limit (e.g., if the memory limit is 2G, the maximum heap size is 500MB).

The JVM option has also been added to give Docker container users fine-grained control over the amount of system memory used for Java heaps.

This support is enabled by default and is only available on Linux-based platforms.

Multi-version jar files

In Java 11, it is possible to create a jar file that contains multiple versions of a class file specific to a Java distribution. With multi-release jar files, library developers can support multiple versions of Java without having to deliver multiple versions of jar files. For users of these libraries, multi-release jar files solve the problem of having to match a specific jar file to a specific runtime target.

Other Performance Improvements

The following changes to the JVM have a direct impact on performance.

  • JEP 197: Segmented Code Cache - splits the code cache into segments. This segmentation improves performance by better controlling the JVM memory footprint, reducing scan time for compiled methods, and significantly reducing code cache fragmentation.
  • JEP 254: Compact string - changes the internal representation of a string from two bytes per character to one or two bytes per character, depending on the character encoding. Since most strings contain the ISO-8859-1/Latin-1 character, this change effectively halves the amount of space needed to store the string.
  • JEP 310: Application Class-Data Sharing The -Class-Data share reduces startup time by allowing memory mapping at runtime. Application Class-Data sharing extends class-data sharing by allowing application classes to be placed in CDS archives. This saves memory and reduces overall system response time when multiple JVMs share the same archive file.
  • JEP 312: Thread-Local Handshake - enables you to perform callbacks on threads without having to perform a global VM safepoint, which helps VMs reduce the number of global safepoints, resulting in lower latency.
  • Delayed allocation of compiler threads - In layered compilation mode, the VM will start a large number of compiler threads. On systems with many CPUs, this is the default mode. These threads are created regardless of the amount of memory available and regardless of how many compilation requests there are. The threads consume memory even when they are idle (which is almost all the time), which leads to inefficient use of resources. To solve this problem, we have changed the implementation to start only one compiler thread of each type at startup. The system will dynamically handle starting other threads and closing unused threads.

The following changes to the core library can affect the performance of new or modified code.

  • JEP 193: Variable Handles - Define a standard method to invoke the equivalent of various util and operations on object fields and array elements, a standard set of fencing operations for precise control of memory ordering, and a standard accessibility guard operation to ensure that referenced objects remain accessible.
  • JEP 269: Convenience Factory Methods for Collections - Defines library api’s that allow you to easily create instances of collections and mappings that contain a small number of elements. This is a static factory method on the collection interface for creating lean and unmodifiable instances of collections. These instances are inherently more efficient. The collections created by these APIs are represented in a concise manner, without wrapper classes.
  • JEP 285: Spin-Wait hints - provides APIs that allow Java to hint that the runtime system is in a spin-loop. Some hardware platforms can take advantage of software indications that a thread is in a “busy-waiting” state.
  • JEP 321: HTTP Client (Standard) - provides a new http client API that implements Http/2 and WebSocket, and replaces the older HttpURLConnection API.

Possible issues with converting Java 8 to Java 11

When converting code from Java 8 to Java 11, there is no one-size-fits-all solution. For less critical applications, migrating from Java 8 to Java 11 can mean a significant amount of work. Potential problems include.

  • Deleted APIs
  • Deprecated packages
  • Use of internal APIs
  • changes to the class loader
  • and changes to garbage collection.

Usually, the solution is to try to run on Java 11 without recompiling, or to compile with JDK 11 first. If the goal is to get the application up and running as soon as possible, the best approach is usually to run it directly on Java 11. For libraries, the goal will be to release projects compiled and tested using JDK 11.

Migrating to Java 11 is worth the effort. Since the release of Java 8, several new features have been added and enhancements have been made to existing features. These features and enhancements improve startup, performance and memory usage, and provide better integration with containers. Additions and changes have also been made to the API, which can improve developer productivity.


Java 11 has two tools for probing potential problems: jdeprscan and jdeps. These two tools can be run on existing classes or jar files. The conversion effort can be evaluated without recompiling.

  • jdeprscan can see if a deprecated or deleted API is being used. Using a deprecated API is not a blocking issue, but is worth exploring. Is there an issue that needs to be logged to resolve the use of the deprecated API? If using the deprecated API is a blocking issue that must be resolved before you can attempt to run the application on Java 11.
  • jdeps, a Java class dependency parser. When used with the --jdk-internals option, jdeps tells you which class depends on which internal API. It is possible to continue using the internal APIs in Java 11, but changing this usage should be a priority. OpenJDK Wiki page Java Dependency Analysis Tool recommends replacements for some common JDK internal APIs.

Both Gradle and Maven have jdeps and jdeprscan plugins. It is recommended that the following tools be added to the generated scripts.

Tools Gradle Plugin Maven Plugin
jdeps jdeps-gradle-plugin Apache Maven JDeps plugin
jdeprscan jdeprscan-gradle-plugin Apache Maven JDeprScan plugin

The Java compiler itself, javac, is another tool in the toolbox. The warnings and errors obtained from jdeprscan and jdeps come from the compiler. The advantage of using jdeprscan and jdeps is that both tools can be run on existing jar and class files (including third-party libraries). What jdeprscan and jdeps cannot do is to warn about the use of reflection to access the wrapped API. Reflection access is checked at runtime. Ultimately the code must be run on Java 11 to know exactly.

Using jdeprscan

To use jdeprscan, the easiest way is to provide it with a jar file from an existing generation. You can also specify a directory (such as the compiler output directory) or a single class name for it. Use the -release 11 option to get the most complete list of deprecated APIs. To determine the priority of deprecated APIs to adopt, roll back the setting to -release 8. Deprecated APIs in Java 8 may be removed earlier than the most recently deprecated APIs.

jdeprscan --release 11 my-application.jar

The jdeprscan tool generates an error message if it is unable to resolve a dependent class. For example error: cannot find class org/apache/logging/log4j/Logger. It is recommended to add the dependent class to --class-path or use the application class-path, but the tool will continue to scan without it. The argument is -class-path. Other variants of the -class-path argument will not 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 class is calling the deprecated constructor of the java.lang. javadoc will suggest an API to replace the deprecated API. There is no way to resolve the “error: cannot find class sun/misc/BASE64Encoder” issue because it is a deprecated API. Since the release of Java 8, you should Use java.util.Base64.

Run jdeprscan --release 11 --list to see the specific APIs deprecated since Java 8. To get a list of deprecated APIs, run jdeprscan --release 11 --list --for-removal.

Using jdeps

You can use jdeps to find dependencies on the JDK’s internal APIs with the --jdk-internals option. This example requires the -multi-release 11 command line option, because log4j-core-2.13.0.jar is a multi-release jar file. Without this option, jdeps will issue an error message if a multi-release jar file is found. This option specifies the version of the class file to check.

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 -> sun.misc.BASE64Encoder JDK internal API (JDK removed internal API) -> sun.misc.Unsafe JDK internal API (jdk.unsupported) -> JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementations 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:

JDK Internal API Suggested Replacement
---------------- ---------------------
sun.misc.BASE64Encoder Use java.util.Base64 @since 1.8
sun.misc.Unsafe See   

The output provides some good advice on avoiding using the JDK’s internal APIs! If possible, it is recommended to use the replacement API. Provide the name of the module that wraps the package in parentheses. If you need to break the wrapper explicitly, use the module name in conjunction with -add-exports or -add-opens. Using sun.misc.BASE64Encoder or sun.misc.BASE64Decoder causes java.lang.NoClassDefFoundError in Java 11. Code using these APIs must be modified to use java.util.Base64.

Try not to use any APIs from the jdk.unsupported module. The APIs in this module will reference JDK Enhancement Program (JEP) 260 as a suggested replacement. In short, JEP 260 states that using the internal API will be supported until the replacement API is available. Although your code is using the JDK internal API, it will work, at least for a while. Take a look at JEP 260, as it points out certain replacements for the internal API. For example, you can use variable handle instead of one of the sun.misc.Unsafe APIs Unsafe API.

In addition to scanning for usage of the JDK’s internal APIs, jdeps can also perform other operations. It is a useful tool for analyzing dependencies and generating module information files. For more information, see the documentation.

Using javac

If you compile with JDK 11, you will need to update it to generate scripts, tools, test frameworks, and included libraries. Use javac’s -Xlint:unchecked option to get usage details and other warnings for the JDK’s internal APIs. It may also be necessary to expose wrapped packages to the compiler using -add-opens or -add-reads (see JEP 261).

Libraries may be considered to be packaged as multi-version jar files. Multi-version jar files allow both Java 8 and Java 11 runtimes to be supported in the same jar file. They increase the complexity of generation. It is beyond the scope of this document to discuss how to generate multi-version jars.

Running on Java 11

Most applications should be able to run on Java 11 without modification. The first thing to try is to run on Java 11 without recompiling the code. The purpose of running it directly is to see what warnings and errors occur when it is executed. This method allows the application to run faster on Java 11 because it minimizes those concerns that must be completed.

Most of the issues you may encounter can be resolved without recompiling the code. If you need to fix the problem in your code, do so, but continue to compile with JDK 8. If possible, have the application 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. Removed options can cause the Java Virtual Machine (JVM) to exit. This check is especially important if using the GC logging options, as they are significantly different from what was the case in Java 8. The JaCoLine tool is a good tool for checking command line options for problems.

Check third-party libraries

Third-party libraries that you do not control are a potential source of problems. You can proactively update third-party libraries to a newer version. It is also possible to see which libraries are not in use when running the application and update only those that are required. The problem with updating all libraries to the latest version is that if there is an error in the application, it is more difficult to find the root cause. Did the error occur because a library was updated? Or was the error caused by some change in the runtime? The problem with updating only what is needed is that it may take several iterations to fix the problem.

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

In addition to checking the release notes, you can use jdeps and jdeprscan to evaluate jar files. In addition, the OpenJDK Quality Group maintains a Quality Outreach Wiki page, which lists the status of tests performed on several Free Open Source Software (FOSS) projects based on OpenJDK versions.

Explicitly setting up garbage collection

The parallel garbage collector (parallel GC) was the default GC in Java 8. If your application uses the default, you should set the GC explicitly using the command line option -XX:+UseParallelGC. The default in Java 9 has been changed to the Garbage First garbage collector (G1GC). For a fair comparison of applications running on Java 8 and Java 11, the GC setting must be the same. Experimentation with GC settings should be deferred until the application has been verified on Java 11.

Explicitly set default options

If running on a point-of-action VM, setting the command line option -XX:+PrintCommandLineFlags dumps the values of the options set by the VM, specifically the default values set by the GC. Run with this flag on Java 8, and use the output options when running on Java 11. For the most part, the defaults are the same in Java 8 through 11. However, using the setting in Java 8 ensures parity.

It is recommended to set the command line option --illegal-access=warn. In Java 11, using reflection to access the JDK internal API generates an “Illegal Reflection Access” warning. By default, the system only warns about the first illegal access. Setting --illegal-access=warn causes the system to warn about every illegal reflection access. If you set the option to warn, more cases of illegal access will be found. However, you will also receive a large number of redundant warnings.

Setting --illegal-access=deny will simulate future behavior of the Java runtime once the application is running on Java 11. Starting with Java 16, the default setting will be --illegal-access=deny.

ClassLoader Caution

In Java 8, it is possible to force the system class loader to be converted to a URLClassLoader. This is typically done by applications and libraries that need to inject classes into the classpath at runtime. The class loader hierarchy has been changed in Java 11. The system class loader (also known as the application class loader) is now an internal class. Forcing a conversion to URLClassLoader raises a ClassCastException at runtime. Java 11 cannot dynamically enhance the classpath at runtime via the API, but this can be done via reflection, which displays a prominent warning about how to use the internal API.

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

Region setting data changes

The default source for locale data in Java 11 has been changed to the Unicode Consortium’s public locale data repository via JEP 252. This may affect the localized formatting. If necessary, set the system property to java.locale.providers=COMPAT,SPI and revert to the Java 8 locale behavior.

Potential problems

Here are some common problems that may be encountered.

  • Unrecognized VM options
  • Unrecognized options
  • VM warning: ignore options
  • VM warning: option
  • Warning: illegal reflection access operation occurred
  • java.lang.reflect.InaccessibleObjectException
  • java.lang.NoClassDefFoundError
  • -Xbootclasspath/p is no longer a supported option
  • java.lang.UnsupportedClassVersionError

Unrecognized options

If a command line option is removed, the application outputs Unrecognized option: or Unrecognized VM option followed by the name of the problematic option. Unrecognized options will cause the VM to exit. Options that have been deprecated but not removed generate VM warnings.

Usually, there is no replacement for a deleted option, and the only way to remove it from the command line is to delete it. The exception is the garbage collection logging option. GC logging has been re-implemented in Java 9 and can be done using the Unified JVM logging framework. See the Java SE 11 Tool Reference for permission to Logging via the JVM Unified Logging Framework section of the Java SE 11 Tool Reference, “Table 2-2 Mapping Old Garbage Collection Logging Flags to Xlog Configuration”.

VM Warnings

Using a deprecated option generates a warning. When an option has been replaced or is no longer useful, it is deprecated. These options should be removed from the command line, as with the use of deleted options. “VM Warning: Option

The Web Page VM Options Explorer provides an exhaustive list of options that have been added or removed in Java since JDK 7.

Error: Unable to create Java Virtual Machine

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

Warning: Illegal Reflection Access Operation Occurred

When Java code uses reflection to access the JDK’s internal APIs, an “Illegal Reflection Access” warning is issued at runtime.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method
WARNING: Please consider reporting this to the maintainers of
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
WARNING: All illegal access operations will be denied in a future release.

This means that the module is not exporting a package that is accessible via reflection. This package is wrapped in the module and is essentially an internal API. When starting and running an application on Java 11, the first action is likely to be to ignore this warning. The Java 11 runtime allows reflection access, so older code can continue to run. To resolve this warning, look for updated code that does not use the internal API. If you cannot resolve the issue with updated code, you can use the -add-exports or -add-opens command line options to enable access to packages. These options allow access to the unexported types of another module from one module.

The --add-exports option allows the target module to access the public types of the named packages of the source module. Sometimes, code will use setAccessible(true) to access non-public members and APIs. This is called deep reflection. In this case, use --add-opens to allow the code to access the non-public members of the package. If you are not sure whether to use --add-exports or --add-opens, start with --add-exports. The --add-exports or --add-opens options should be viewed as a stop-gap solution, not a long-term solution. Using these options breaks the encapsulation of the module system, which is designed to prevent the JDK internal API from being used. If the internal API is removed or changed, the application will fail. Java 16 will deny reflection access except when access is enabled via command line options such as -add-opens. To simulate future behavior, set --illegal-access=deny on the command line.

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


This exception instructs you to try to call setAccessible(true) on a field or method of an encapsulated class. You may also receive an “illegal reflection access” warning. Use the -add-opens option to give code access to non-public members of the package. The exception message will tell you that the module did not open the package to the module trying to call setAccessible. If the module is an 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.


NoClassDefFoundError is most likely caused by splitting a package or referencing a deleted module.

NoClassDefFoundError caused by splitting a package

If a package is found in multiple libraries, it is a split package. The symptom of the split package problem is that you know a class will be on the class-path, but cannot find it.

This problem only occurs when using module-path. The Java module system optimizes class lookup by restricting packages to a named module. When performing a class lookup, the runtime will give priority to module-path over class-path. If a package is split between a module and class-path, only that module will be used to perform class lookups. This may result in a NoClassDefFound error.

To check for split packages, a simple way is to insert the module path and class path into jdeps, using the path to the application class file as . If there is a split package, jdeps will output a warning: Warning: split package:. This can be fixed by adding the split package to the named module using -patch-module =[,].

NoClassDefFoundError caused by using Java EE or CORBA modules

If an application is running on Java 8 but raises java.lang.NoClassDefFoundError or java.lang.ClassNotFoundException, it is possible that the application is using packages from a Java EE or CORBA module. These modules were deprecated in Java 9 and removed in Java 11.

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

Removed Modules Affected Packages Suggested Dependencies
Java API for XML Web Services (JAX-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 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. Please use --patch-module instead. The -patch-module option is described in JEP 261. Look for the section labeled “Patch Module Contents”. You can use -patch-module with javac and java to rewrite or enhance classes in a module.

In effect, -patch-module performs the operation of inserting the patch module into the module system’s class lookup. The module system will first fetch the classes from the patch module. This has the same effect as prepending the bootclasspath in Java 8.


This exception indicates that you are trying to run code compiled with a higher version of Java on a lower version of Java. For example, running its jar on Java 11 is a program compiled using JDK 13.

Java Version Class File Format Version
8 52
9 53
10 54
11 55
12 56
13 57

Next steps

After running the application on Java 11, consider moving the library out of class-path and then into module-path. Find the updated version of the library on which the application depends. Select the module library (if available). Use module-path whenever possible, even if you do not intend to use modules in your application. Use module-path for better class loading performance than class-path.