Part 13 of 14

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

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

LibraryMinimum Java 17 compatible version
Spring Boot2.7.x (Java 17 support) / 3.x (requires Java 17)
Spring Framework5.3.x (Java 17 support) / 6.x (requires Java 17)
Hibernate5.6.x / 6.x
Jackson2.13+
Mockito4.x+
Byte Buddy1.12+
ByteBuddy (transitively via Mockito)1.12+
Lombok1.18.22+
Guava31+
ASM9.x
cglibNot compatible — upgrade to ByteBuddy-based alternatives
Javassist3.28+
Kryo5.x
Log4j22.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 messageLibraryFix
java.util.HashMap inaccessibleKryo < 5Upgrade Kryo to 5.x
java.lang.reflect.Method inaccessibleMockito < 4Upgrade Mockito to 5.x
java.lang.Class inaccessibleLombok < 1.18.22Upgrade Lombok
sun.reflect.ReflectionFactorycglibReplace 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.BASE64Encoderjava.util.Base64
sun.misc.BASE64Decoderjava.util.Base64
sun.reflect.ReflectionFactoryjava.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)

ToolStatus in Java 17
javahRemoved (use javac -h instead)
javapackagerMoved to JavaFX SDK
native2asciiRemoved
extcheckRemoved
jhatRemoved (use JFR + JMC)
jrunscriptAvailable 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:

  1. Start with leaf modules (fewest dependencies, no internal dependents)
  2. Compile with Java 17, fix errors, run tests
  3. Commit and tag
  4. 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.