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:
- Make it compile and run — fix incompatibilities introduced by Java 9–11
- 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:
| Library | Minimum Java 11 Version |
|---|---|
| Spring Framework | 5.1.0 |
| Spring Boot | 2.1.0 |
| Hibernate ORM | 5.3.0 |
| Jackson | 2.9.6 |
| Mockito | 2.23.0 |
| JUnit 5 | 5.3.1 |
| JUnit 4 | 4.12 (works with Surefire 2.22+) |
| Byte Buddy | 1.9.0 |
| ASM | 7.0 |
| cglib | 3.2.8 |
| Javassist | 3.23.1 |
| Lombok | 1.18.4 |
| MapStruct | 1.3.0 |
| Guava | 27.0 |
| Log4j 2 | 2.11.0 |
| SLF4J | 1.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/-targetflags
<!-- 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:
| Failure | Cause | Fix |
|---|---|---|
ClassNotFoundException: javax.xml.bind.JAXBContext | JAXB removed | Add Jakarta JAXB dependency |
InaccessibleObjectException | Module encapsulation | Add --add-opens |
NoClassDefFoundError: sun/misc/BASE64Encoder | Internal API removed | Replace with java.util.Base64 |
UnsupportedClassVersionError | Wrong --release level | Set --release 11 |
IllegalArgumentException: Unsupported class file major version 55 | Old ASM version | Upgrade ASM to 7.0+ |
Step 10: Test the Deployment
Before going to production:
- Build a complete deployment artifact (JAR, WAR, or Docker image) targeting Java 11.
- Deploy to a staging environment running Java 11.
- Run smoke tests covering all major user paths.
- 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).
- Check startup time — AppCDS can help if startup has regressed.
Common Migration Issues Reference
| Error | Root Cause | Fix |
|---|---|---|
java.lang.ClassNotFoundException: javax.xml.bind.* | JAXB removed (JEP 320) | Add jakarta.xml.bind-api + jaxb-impl |
java.lang.reflect.InaccessibleObjectException | Module encapsulation | Add --add-opens |
java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder | Internal API removed | Use java.util.Base64 |
com.sun.xml.internal.bind.* | JAXB internal impl removed | Switch to external JAXB |
Maven build: Unrecognized source/target | Old compiler plugin | Upgrade to 3.6.0+, use --release |
Gradle: Could not find method release() | Gradle < 4.7 | Upgrade Gradle |
| Tests hang or fail mysteriously | Library using unsupported --add-opens | Identify library and upgrade |
Error: LinkageError occurred while loading main class | Split package | Use jdeps to find duplicate packages |
What’s Next
Next: Java 11 Production Checklist and Performance Best Practices