Part 15 of 16

Migrating to Java 11: From Java 8 — Step by Step

Migration Overview

Migrating from Java 8 to Java 11 is the most common Java upgrade scenario. The migration has two distinct phases:

  1. Make it compile and run — fix incompatibilities introduced by Java 9–11
  2. Modernise the code — adopt var, List.of(), String.isBlank(), and other new APIs

Phase 1 is mandatory and blocks the upgrade. Phase 2 is optional and can happen incrementally.

This guide focuses entirely on Phase 1.


The 10-Step Migration Checklist

[ ] 1. Audit dependencies with jdeps
[ ] 2. Update build tools (Maven 3.6.1+, Gradle 4.7+)
[ ] 3. Update library dependencies to Java 11-compatible versions
[ ] 4. Add Jakarta JAXB/JAX-WS dependencies (if used)
[ ] 5. Fix internal API usage (sun.misc, sun.reflect)
[ ] 6. Add --add-opens for frameworks that need reflection
[ ] 7. Replace removed GC flags with new -Xlog:gc* syntax
[ ] 8. Compile with --release 11
[ ] 9. Run the full test suite
[ ] 10. Test with a staging deployment

Step 1: Audit Dependencies with jdeps

jdeps analyses a JAR and reports which packages it depends on — including internal JDK packages that are restricted in Java 11.

# Audit your application JAR and all its dependencies
jdeps \
  --multi-release 11 \
  --class-path 'target/dependency/*' \
  --ignore-missing-deps \
  myapp.jar

# Check for internal JDK dependencies
jdeps \
  --multi-release 11 \
  --class-path 'target/dependency/*' \
  --jdk-internals \
  myapp.jar

Sample output:

myapp.jar -> java.base
myapp.jar -> java.xml.bind  (WARNING — removed in Java 11)
   com.example.model.User -> javax.xml.bind.annotation.XmlRootElement
   com.example.service.XmlService -> javax.xml.bind.JAXBContext

myapp.jar -> JDK internal APIs:
   com.example.util.Encoder -> sun.misc.BASE64Encoder (JDK internal)

This tells you exactly which classes use removed/internal APIs. Fix these before moving on.

Find all dependency JARs (Maven)

mvn dependency:copy-dependencies -DoutputDirectory=target/dependency
jdeps --multi-release 11 \
      --class-path 'target/dependency/*' \
      --jdk-internals \
      target/myapp.jar

Step 2: Upgrade Build Tools

Maven

Minimum: Maven 3.6.1. Recommended: Maven 3.9.x.

# Check current version
mvn -version

# Upgrade via SDKMAN
sdk install maven 3.9.6
sdk use maven 3.9.6

Update pom.xml compiler configuration:

<properties>
  <maven.compiler.release>11</maven.compiler.release>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.12.1</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.5</version>
    </plugin>
  </plugins>
</build>

Gradle

Minimum: Gradle 4.7. Recommended: Gradle 7.6.x or 8.x.

# Update the Gradle wrapper
./gradlew wrapper --gradle-version=8.7

# build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(11))
    }
}

Step 3: Update Library Dependencies

Use the minimum Java 11-compatible versions for all bytecode-manipulation libraries:

LibraryMinimum Java 11 Version
Spring Framework5.1.0
Spring Boot2.1.0
Hibernate ORM5.3.0
Jackson2.9.6
Mockito2.23.0
JUnit 55.3.1
JUnit 44.12 (works with Surefire 2.22+)
Byte Buddy1.9.0
ASM7.0
cglib3.2.8
Javassist3.23.1
Lombok1.18.4
MapStruct1.3.0
Guava27.0
Log4j 22.11.0
SLF4J1.7.25

Update all of these before attempting to compile against Java 11.


Step 4: Add Jakarta JAXB / JAX-WS

If your app uses JAXB (javax.xml.bind.*) or JAX-WS (javax.xml.ws.*), add the external dependencies:

JAXB (most common)

<!-- pom.xml -->
<dependencies>
  <!-- JAXB API -->
  <dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.3</version>
  </dependency>
  <!-- JAXB Runtime -->
  <dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.3</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

No code changes needed — the packages are still javax.xml.bind.* in the 2.x versions.

javax.annotation (@PostConstruct, @Resource)

<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>1.3.5</version>
</dependency>

Step 5: Fix Internal API Usage

Replace sun.* and com.sun.* internal API calls:

Base64 (very common)

// Old — breaks on Java 11
import sun.misc.BASE64Encoder;
String encoded = new BASE64Encoder().encode(bytes);

// New — available since Java 8
import java.util.Base64;
String encoded = Base64.getEncoder().encodeToString(bytes);
String decoded = Base64.getDecoder().decode(encodedString);

Reflection

// Old
Class<?> caller = sun.reflect.Reflection.getCallerClass();

// New (Java 9+)
Class<?> caller = StackWalker
    .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
    .getCallerClass();

XML parsing (internal DocumentBuilder)

// Old — using internal SAX parser
import com.sun.org.apache.xerces.internal.impl.xs.XSDeclarationPool;

// New — use the public javax.xml.parsers API
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

Step 6: Add –add-opens for Framework Reflection

Spring, Hibernate, and other frameworks use reflection to access private fields. With the module system’s stronger encapsulation, these frameworks need explicit permission.

Add to your startup script or JVM flags in your CI/CD configuration:

java \
  --add-opens java.base/java.lang=ALL-UNNAMED \
  --add-opens java.base/java.util=ALL-UNNAMED \
  --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
  --add-opens java.base/java.io=ALL-UNNAMED \
  --add-opens java.rmi/sun.rmi.transport=ALL-UNNAMED \
  -jar myapp.jar

For Spring Boot, put these in JAVA_OPTS or in spring-boot-maven-plugin:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <jvmArguments>
      --add-opens java.base/java.lang=ALL-UNNAMED
      --add-opens java.base/java.util=ALL-UNNAMED
    </jvmArguments>
  </configuration>
</plugin>

Note: Spring Boot 2.6+ and Spring Framework 5.3+ do not require --add-opens for their own internals. The requirement comes from third-party libraries using older bytecode manipulation techniques.


Step 7: Migrate GC Flags

Java 9 replaced all old GC logging flags with the unified -Xlog system.

Flag translation

# Old Java 8 flags → Java 11 equivalents

# Verbose GC logging
-verbose:gc
→ -Xlog:gc

# Detailed GC with heap info
-XX:+PrintGCDetails
→ -Xlog:gc*

# With timestamps and file output
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/var/log/gc.log
→ -Xlog:gc*:file=/var/log/gc.log:time,uptime,pid:filecount=5,filesize=20m

# GC pauses only
-XX:+PrintGCApplicationStoppedTime
→ -Xlog:safepoint

# Object ages
-XX:+PrintTenuringDistribution
→ -Xlog:gc+age*=debug

Removed flags

These flags were silently ignored in Java 9 but cause errors in Java 11:

# Do NOT use in Java 11 — these flags are removed:
-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:MaxPermSize=256m        # PermGen is gone; use -XX:MaxMetaspaceSize=
-XX:PermSize=128m           # Gone

Step 8: Compile with –release 11

Add --release 11 to your compiler configuration. This:

  • Targets Java 11 bytecode
  • Prevents use of APIs from Java versions > 11
  • Replaces older -source/-target flags
<!-- Maven -->
<properties>
  <maven.compiler.release>11</maven.compiler.release>
</properties>
// Gradle Kotlin DSL
tasks.withType<JavaCompile>().configureEach {
    options.release.set(11)
}

Step 9: Run the Full Test Suite

mvn clean test

# Or with --add-opens for test-time reflection
mvn clean test \
  -DargLine="--add-opens java.base/java.lang=ALL-UNNAMED \
             --add-opens java.base/java.util=ALL-UNNAMED"

Common test failures on Java 11:

FailureCauseFix
ClassNotFoundException: javax.xml.bind.JAXBContextJAXB removedAdd Jakarta JAXB dependency
InaccessibleObjectExceptionModule encapsulationAdd --add-opens
NoClassDefFoundError: sun/misc/BASE64EncoderInternal API removedReplace with java.util.Base64
UnsupportedClassVersionErrorWrong --release levelSet --release 11
IllegalArgumentException: Unsupported class file major version 55Old ASM versionUpgrade ASM to 7.0+

Step 10: Test the Deployment

Before going to production:

  1. Build a complete deployment artifact (JAR, WAR, or Docker image) targeting Java 11.
  2. Deploy to a staging environment running Java 11.
  3. Run smoke tests covering all major user paths.
  4. Monitor GC logs for unexpected Full GC events (G1GC is the new default — it may behave differently from Java 8’s Parallel GC for your workload).
  5. Check startup time — AppCDS can help if startup has regressed.

Common Migration Issues Reference

ErrorRoot CauseFix
java.lang.ClassNotFoundException: javax.xml.bind.*JAXB removed (JEP 320)Add jakarta.xml.bind-api + jaxb-impl
java.lang.reflect.InaccessibleObjectExceptionModule encapsulationAdd --add-opens
java.lang.NoClassDefFoundError: sun/misc/BASE64EncoderInternal API removedUse java.util.Base64
com.sun.xml.internal.bind.*JAXB internal impl removedSwitch to external JAXB
Maven build: Unrecognized source/targetOld compiler pluginUpgrade to 3.6.0+, use --release
Gradle: Could not find method release()Gradle < 4.7Upgrade Gradle
Tests hang or fail mysteriouslyLibrary using unsupported --add-opensIdentify library and upgrade
Error: LinkageError occurred while loading main classSplit packageUse jdeps to find duplicate packages

What’s Next

Next: Java 11 Production Checklist and Performance Best Practices