Migrating to Java 17: From Java 8 and Java 11 — Step by Step
Why Migrate to Java 17?
Java 17 is the current recommended enterprise LTS. Key reasons to migrate now:
- Spring Boot 3.x requires Java 17 — the entire Spring ecosystem is moving here
- Java 11 extended support ends around 2026 depending on your vendor
- Java 8 mainstream support ended in 2019; extended support ended 2030 for Oracle JDK but active vulnerability exposure is increasing
- Language features: Records, Sealed Classes, Text Blocks, Pattern Matching, Switch Expressions — all available in Java 17
- Security: Strong JDK encapsulation closes years of internal API exposure used in exploits
Recommended Migration Path
Java 8 → Java 11 → Java 17
Do not jump directly from Java 8 to Java 17 in one step if your codebase is large or has many third-party dependencies. Each hop surfaces a different set of issues in isolation.
Exception: If your codebase is well-tested and has few dependencies that access JDK internals, a direct Java 8 → Java 17 jump is feasible. Assess per project.
Step 1: Audit Dependencies
Before upgrading the JDK, check whether your dependencies support Java 17.
Using jdeps
# Check what JDK internal APIs your code and dependencies use
jdeps --jdk-internals --multi-release 17 \
--class-path "$(ls lib/*.jar | tr '\n' ':')" \
target/myapp.jar
Any output under “JDK Internal APIs” identifies code that will fail with InaccessibleObjectException on Java 17.
Using jdeprscan
# Find usage of APIs deprecated for removal in Java 17
jdeprscan --release 17 --for-removal target/myapp.jar
Critical Library Versions for Java 17
| Library | Minimum Java 17 compatible version |
|---|---|
| Spring Boot | 2.7.x (Java 17 support) / 3.x (requires Java 17) |
| Spring Framework | 5.3.x (Java 17 support) / 6.x (requires Java 17) |
| Hibernate | 5.6.x / 6.x |
| Jackson | 2.13+ |
| Mockito | 4.x+ |
| Byte Buddy | 1.12+ |
| ByteBuddy (transitively via Mockito) | 1.12+ |
| Lombok | 1.18.22+ |
| Guava | 31+ |
| ASM | 9.x |
| cglib | Not compatible — upgrade to ByteBuddy-based alternatives |
| Javassist | 3.28+ |
| Kryo | 5.x |
| Log4j2 | 2.17.1+ (also patches Log4Shell) |
Step 2: Install Java 17
# SDKMAN (recommended)
sdk install java 17.0.11-tem
sdk default java 17.0.11-tem
# Verify
java -version
# openjdk version "17.0.11" 2024-04-16
See Article 2 for full installation options.
Step 3: Update Build Tool Configuration
Maven
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
Use <release> not <source>+<target>. The release flag is stricter and correctly prevents use of newer APIs.
Gradle
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.withType<JavaCompile> {
options.release = 17
}
Step 4: Run the Build and Fix Compile Errors
mvn clean compile
Compile Error: Removed JDK Packages
JAXB (removed in Java 11, not in Java 17 — but affects Java 8 → Java 17 jumps):
package javax.xml.bind does not exist
Fix:
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.4</version>
<scope>runtime</scope>
</dependency>
Java Activation Framework (JAF):
<dependency>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<version>2.1.2</version>
</dependency>
RMI Activation (removed in Java 17):
package java.rmi.activation does not exist
No library replacement — rewrite using gRPC, REST, or modern RPC. See Article 11.
Compile Error: Deprecated Methods Removed
Thread.stop(), Thread.suspend(), Thread.resume() — removed in Java 21, but deprecated-for-removal since Java 1.2. Identify and replace with interrupt().
Runtime.traceInstructions(), Runtime.traceMethodCalls() — removed.
Step 5: Run Tests and Fix Runtime Errors
mvn test
Common Runtime Failure: InaccessibleObjectException
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final ... accessible:
module java.base does not "opens java.util" to unnamed module
This is the most common Java 17 migration failure. It means a library is using JDK internal APIs.
Temporary fix — add to Surefire argLine:
<configuration>
<argLine>
--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
</argLine>
</configuration>
Root fix — upgrade the library. Common causes and fixes:
| Exception message | Library | Fix |
|---|---|---|
java.util.HashMap inaccessible | Kryo < 5 | Upgrade Kryo to 5.x |
java.lang.reflect.Method inaccessible | Mockito < 4 | Upgrade Mockito to 5.x |
java.lang.Class inaccessible | Lombok < 1.18.22 | Upgrade Lombok |
sun.reflect.ReflectionFactory | cglib | Replace with ByteBuddy |
Common Runtime Failure: NoSuchMethodError
A library calls a method that was removed:
java.lang.NoSuchMethodError: 'void java.lang.Thread.stop()'
The library needs to be upgraded to a version that doesn’t call removed methods.
Common Runtime Failure: ClassNotFoundException
A removed module’s class is referenced:
java.lang.ClassNotFoundException: com.sun.activation.registries.MailcapFile
Add the Jakarta EE dependency for the missing class.
From Java 8: Additional Steps
If jumping from Java 8 (not Java 11), you have additional changes beyond Java 17’s own changes:
Remove Unsafe System.in, System.out Applet-Era APIs
These were never documented but some libraries used them. Replace with standard I/O.
Update sun.* Package Usages
sun.* packages were internal in Java 8 and some were removed in Java 9+. Replace with public APIs:
| Old (sun.*) | Replacement |
|---|---|
sun.misc.BASE64Encoder | java.util.Base64 |
sun.misc.BASE64Decoder | java.util.Base64 |
sun.reflect.ReflectionFactory | java.lang.reflect |
com.sun.net.ssl.* | javax.net.ssl.* |
Update Endorsed Standards Override
The endorsed standards directory mechanism was removed in Java 9. If you used -Djava.endorsed.dirs, remove it and replace the override mechanism.
PermGen is Gone
Java 8 → Java 11 introduced Metaspace (replacing PermGen). If your JVM flags include -XX:MaxPermSize, remove them and add -XX:MaxMetaspaceSize if needed:
# Remove
-XX:PermSize=256m
-XX:MaxPermSize=512m
# Replace with (only if you were hitting PermGen limits)
-XX:MaxMetaspaceSize=512m
Tool Changes (Java 8 → Java 17)
| Tool | Status in Java 17 |
|---|---|
javah | Removed (use javac -h instead) |
javapackager | Moved to JavaFX SDK |
native2ascii | Removed |
extcheck | Removed |
jhat | Removed (use JFR + JMC) |
jrunscript | Available but Nashorn removed |
jjs (Nashorn) | Removed |
Step 6: Update GC Configuration
If You Are Using CMS GC
CMS was removed in Java 14. If your JVM flags include -XX:+UseConcMarkSweepGC:
# Remove this:
-XX:+UseConcMarkSweepGC
# Replace with G1GC (default since Java 9):
-XX:+UseG1GC
# Or ZGC for low-latency workloads:
-XX:+UseZGC
Update G1GC Flags
If using G1GC flags from Java 8, some were removed or renamed:
# Removed: -XX:+UseG1GC (it's the default now; not needed)
# Removed: -XX:G1HeapRegionSize (use -XX:G1HeapRegionSize=N to override, else let JVM auto-size)
# Removed: -XX:InitiatingHeapOccupancyPercent (still valid, just less tuning needed)
ZGC in Java 17
ZGC became production-ready in Java 15. Java 17 includes a production-ready ZGC:
-XX:+UseZGC
Note: Generational ZGC (the major improvement) is a Java 21 feature. The Java 17 ZGC is non-generational.
Step 7: Validate Application Behavior
Run your integration tests and end-to-end tests — not just unit tests. Module system issues often only surface at runtime when specific code paths execute.
mvn verify # runs unit + integration tests
For Spring Boot applications, run the application and exercise key API endpoints:
java -jar target/myapp.jar &
curl http://localhost:8080/health
curl http://localhost:8080/api/users
Step 8: Performance Validation
Run a baseline load test before and after the migration:
# Before (Java 11)
java -jar app.jar &
wrk -t4 -c200 -d30s http://localhost:8080/api/endpoint
# After (Java 17)
java -XX:+UseZGC -jar app.jar &
wrk -t4 -c200 -d30s http://localhost:8080/api/endpoint
Monitor:
- Throughput (RPS) — should be equal or better
- P99 latency — should be equal or better with ZGC
- GC overhead — check GC logs for time-in-GC percentage
Enable GC logging:
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m
Incremental Migration for Large Codebases
For large monorepos or multi-module Maven projects, migrate one module at a time:
- Start with leaf modules (fewest dependencies, no internal dependents)
- Compile with Java 17, fix errors, run tests
- Commit and tag
- Move to the next module
This limits the blast radius of any migration issue to one module at a time.
Migration Checklist
Pre-Migration
[ ] Audit: jdeps --jdk-internals myapp.jar
[ ] Audit: jdeprscan --release 17 --for-removal myapp.jar
[ ] Identify and upgrade incompatible libraries
[ ] Install Java 17 JDK (Temurin recommended)
Build Configuration
[ ] Update Maven compiler plugin to release=17
[ ] Update Gradle toolchain to Java 17
[ ] Update maven-surefire-plugin to 3.x
Compile Fixes
[ ] Add JAXB dependency if migrating from Java 8
[ ] Add JAF dependency if migrating from Java 8
[ ] Remove RMI Activation usage
[ ] Remove sun.* package usage
[ ] Remove -XX:MaxPermSize JVM flags
Runtime / Test Fixes
[ ] Add --add-opens for each InaccessibleObjectException (temporary)
[ ] Upgrade libraries causing InaccessibleObjectException (permanent)
[ ] Fix NoSuchMethodError by upgrading libraries
[ ] Fix ClassNotFoundException by adding Jakarta EE dependencies
GC / JVM Flags
[ ] Remove -XX:+UseConcMarkSweepGC; replace with G1GC or ZGC
[ ] Remove -XX:MaxPermSize; add -XX:MaxMetaspaceSize if needed
[ ] Remove --illegal-access (invalid in Java 17)
Validation
[ ] Full unit test suite passes
[ ] Full integration test suite passes
[ ] Load test shows equal or better throughput/latency
[ ] GC logs show healthy collection behavior
What’s Next
Article 14: Java 17 Production Checklist and Performance Best Practices covers what to configure and monitor once your application is running on Java 17 in production.