You can migrate directly from Java 8 to Java 17. The safest way to organize the work is to separate runtime compatibility, build compatibility, and adoption of new language features. That makes each failure easier to trace.
Java 17 is an LTS release, but it is not the newest LTS option. Choose it when your framework, vendor support, or deployment requirements call for it. If Java 21 is the intended destination, use the Java 8 to 21 migration guide alongside your dependency assessment.
What Breaks Between Java 8 and Java 17?
| Symptom | What to investigate |
|---|---|
InaccessibleObjectException | A library or application is reflecting into a package that is not open. |
Missing javax.xml.bind or javax.xml.ws classes | JAXB and JAX-WS are no longer bundled with the JDK. |
ClassCastException involving URLClassLoader | Code assumes the application class loader has its Java 8 implementation. |
| Unsupported class-file version | The runtime or a bytecode-processing library cannot read the generated bytecode. |
| Missing Nashorn script engine | The engine was removed before Java 17. |
| JVM rejects an old startup option | Deployment scripts still contain flags for removed JVM features. |
Use the Oracle JDK 17 migration guide to investigate compatibility changes and the removed tools and components reference for removed features.
Step 1: Inventory the Application and Its Runtime
Capture the production JDK vendor and patch, JVM flags, container image, build wrapper, annotation processors, instrumentation agents, and framework version. Include startup scripts and scheduled jobs, not just the main service.
java -version
./mvnw -version
./mvnw dependency:tree > dependencies.txt
# For Gradle projects:
./gradlew --version
./gradlew dependencies > dependencies.txt
Review libraries that generate or inspect bytecode, including Lombok, Mockito, Byte Buddy, ASM, and monitoring agents. Check the compatibility documentation for each version you actually use. A generic minimum-version list can miss interactions between the framework and its managed dependencies.
For Gradle, check both the JVM running Gradle and the compiler toolchain. The Gradle compatibility matrix lists Java 17 runtime support from Gradle 7.3; your framework plugins may impose additional constraints.
Step 2: Check JDK Internals and Reflection
Run jdeps from the target JDK against your application and dependencies. For a conventional JAR with dependencies in lib/:
jdeps --jdk-internals --multi-release 17 \
--class-path 'lib/*' application.jar
Executable archives with nested dependencies may need to be unpacked first. Static analysis also cannot discover every reflective access, so exercise startup, serialization, proxies, and rarely used integration paths on Java 17.
Fix InaccessibleObjectException at Its Source
JEP 403 strengthens encapsulation of JDK internals. Prefer upgrading the library responsible for the access. If that cannot happen immediately, an exception can be scoped to the package named in the failure:
# Example only: use the package identified by your actual stack trace.
java --add-opens java.base/java.lang=ALL-UNNAMED -jar application.jar
Document which dependency requires each exception and when you can remove it. Replacing reflection with MethodHandles.privateLookupIn does not bypass module access rules.
Do not use --illegal-access=permit, warn, or debug as a Java 17 workaround. That option no longer relaxes access. Also avoid treating every internal API as removed: JEP 403 explicitly leaves critical APIs such as sun.misc.Unsafe available, although depending on internals still deserves review.
Step 3: Add Removed APIs Without Mixing Namespaces
Java 11 removed the bundled Java EE and CORBA modules, including JAXB and JAX-WS, as described in JEP 320. An application that relied on the JDK to supply them must now declare the needed libraries.
For JAXB, distinguish a JDK upgrade from a framework namespace migration:
| Existing application imports | Compatible JAXB namespace family |
|---|---|
javax.xml.bind.* | JAXB 2.3.x API and matching runtime |
jakarta.xml.bind.* | JAXB 3.x or later API and matching runtime |
Adding a Jakarta JAXB 3 dependency will not restore a missing javax.xml.bind.JAXBContext. Keep the API and implementation compatible, select an appropriate patched release, and test real XML payloads. The JAXB release documentation covers the project and its dependencies.
If your framework migration also requires jakarta.*, plan that explicitly. Our Jakarta migration guide covers the separate Spring Boot 3 transition.
Step 4: Run Existing Bytecode on Java 17
Start with the existing Java 8 build artifact and launch it with the Java 17 runtime in a test environment:
/path/to/jdk-17/bin/java -jar application.jar
Fix startup flags, runtime dependencies, reflective access, and integration failures before changing the application’s language level. Test outbound TLS connections, certificate stores, JDBC drivers, authentication, serialization, and batch processing.
You do not need to add module-info.java to complete this migration. Keep classpath deployment unless named modules solve a separate requirement for your application.
Step 5: Move the Build to Java 17
Use the target JDK for the build and configure the compiler’s release level. With a Maven Compiler Plugin version that supports release, the relevant property is:
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
Check parent-POM overrides and the effective plugin configuration. Update test runners, coverage tooling, and annotation processors along with the compiler.
For a small standalone source file, the equivalent command is:
/path/to/jdk-17/bin/javac --release 17 Example.java
If you temporarily need Java 8 compatibility, --release 8 restricts compilation to that API and bytecode level. The resulting application and its dependencies still need tests on every runtime you support.
Adopt records, text blocks, and other language changes after the runtime migration is stable. Keeping those edits separate makes review and rollback easier.
Step 6: Automate Repeatable Edits
OpenRewrite’s Java 17 migration recipe is org.openrewrite.java.migrate.UpgradeToJava17. The recipe documentation describes its build-file and API changes and the current artifact setup.
After configuring the plugin and recipe dependency, start with a dry run:
./mvnw rewrite:dryRun \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava17
Inspect the generated patch before applying it. Record the plugin and recipe versions so the team can reproduce the result. Automated edits do not establish that external integrations, reflection, or operational behavior are compatible.
Step 7: Measure Performance Before Changing Capacity
Run the same workload against the old and new runtimes with comparable CPU limits, heap settings, datasets, and external services. Keep framework upgrades and collector experiments separate where possible.
| Measurement | What to compare |
|---|---|
| Throughput | Successful requests per second at the same latency target |
| CPU | CPU time per successful request, plus throttling |
| Memory | Heap after collection and total process memory |
| Latency | p50, p95, and p99 under steady and burst load |
| Startup | Time until ready and time until normal throughput |
| Reliability | Error rate, timeouts, and resource exhaustion |
Java 17 uses unified JVM logging. For a representative test run:
java -Xlog:gc*:file=gc-java17.log:time,uptime,level,tags \
-jar application.jar
The JDK 17 garbage collection guide explains the collectors and their tradeoffs. Collector availability can also depend on your JDK distribution. Measure the default configuration before adding tuning flags copied from a different workload.
Production Rollout Checklist
- Build, tests, and production use the intended JDK vendor and patch.
- Runtime dependencies and instrumentation agents support Java 17.
- Removed APIs are supplied explicitly with the correct namespace.
- Every remaining
--add-opensexception has an owner and removal plan. - Startup scripts contain no obsolete JVM options.
- Integration and performance tests meet the application’s acceptance criteria.
- A canary deployment receives representative traffic with error and latency monitoring.
- The previous artifact and runtime image are available for rollback.
A runtime rollback will not reverse a database migration. Keep database changes compatible with both versions during the rollout, or test a separate recovery procedure.
If you need help finding compatibility blockers or planning a staged upgrade, discuss your Java migration with Katyella.
Related Articles
- Java 8 to 21 Migration Guide
- Spring Boot 2.7 to 3.x Migration Guide
- Jakarta EE Migration for Spring Boot 3
- Java 25 Performance: What to Measure
Java Modernization Readiness Assessment
15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.