Azul Statement on the eMudhra Intermediate CA Certificates Compromise
jdk-migration-bg

Java 8 to Java 17 Migration: A Step-by-Step Guide for Teams Mid-Upgrade

Smart Summary

This guide walks teams that are partway through a Java 8 to Java 17 migration through the six steps that remain: scanning dependencies, replacing removed APIs, fixing module-encapsulation errors, updating JVM flags, choosing a JDK build, and testing and rolling out in waves. It explains why most failures only appear at runtime and how to keep each one easy to trace. It is most useful for engineers and platform leads who need a practical checklist and a clear view of when to change JDK vendors.

To finish a Java 8 to Java 17 migration, work through six steps in order:

Scan first: run jdeps and jdeprscan, and find libraries too old for 17.

Replace removed APIs: add JAXB and the other Java EE modules back as dependencies.

Fix encapsulation errors: upgrade the library before reaching for –add-opens.

Update JVM flags: drop the options 17 won’t accept and retest GC.

Choose your JDK build: switch vendors separately, at the same version.

Test on that build: run old artifacts on 17 first, then roll out in waves.

Most of these problems show up at runtime, long after your first clean compile.

What Slows Teams Down Between Java 8 and Java 17

When Netflix upgraded its Java applications from 8 to 17, it saw about 20% better CPU usage without any code changes. An old Guice-based framework, outdated Java EE APIs, and unmaintained libraries held up its upgrade.

Those same problems are probably why you’re stuck halfway. You’ve got some services on 17 already, while the rest fail on a removed module or a reflection error. And with Oracle’s roadmap showing that Premier Support for Java 17 ended in September 2026, someone’s asking about the JDK vendor too. This guide covers what’s left, in an order that keeps each failure easy to trace.

Azul Helps Users Migrate From Java 8 to Java 17 With Ease

We’re the only vendor 100% focused on Java, with the largest Java engineering team outside Oracle. Our customers include 37% of the Fortune 100, and we support Java 8 until December 2030 and Java 17 until September 2029. Skybox Security used our builds to go from Java 8 to Java 21 with no problems. That’s the same jump you’re making, one LTS release further.

What Is a Java 8 to Java 17 Migration?

A Java 8 to Java 17 migration means upgrading an application’s build, dependencies, and runtime from JDK 8 to JDK 17. JDK 17 is the LTS release that reached general availability on September 14, 2021. It’s nine feature releases past 8, so you inherit every removal and changed default from 9 to 17 at once.

That’s two jobs in one. You’re running old bytecode on a JDK 17 runtime and compiling your source for 17. If your code sticks to official Java SE APIs, Oracle’s migration guide says it “should continue to work without change.”

How to Migrate From Java 8 to Java 17

If you’re partway through, treat these steps as a checklist and pick up wherever your services are stuck.

1. Scan Your Code and Dependencies Before You Change Anything

Your own code is usually the easy part. But one team on Reddit found many incompatible open-source libraries in its upgrade from 8 to 17. So start by listing what you depend on.

Run these from a JDK 17 install against your built JARs:

jdeps –jdk-internals –class-path ‘lib/*’ app.jar lists every class that depends on a JDK internal API.

jdeprscan –release 17 –for-removal app.jar flags Java SE APIs that are deprecated for removal.

jdeps is a static tool, so it won’t catch internal APIs called through reflection. Steps 3 and 6 pick those up.

Then check when each library first supported Java 17:

Lombok added JDK 17 support in 1.18.22, and ByteBuddy in 1.10.21.

Spring Boot 2.5.5 is the first 2.x release listing Java 17 support. Spring Boot 3.0 needs 17 and switches to Jakarta EE, so treat it as its own project.

Build tools need maven-compiler-plugin 3.6 or later for maven.compiler.release, and Gradle 7.3 is the first to run on 17.

You’re done when every internal API hit and outdated library has a ticket and an owner.

2. Replace the APIs Java Removed

JDK 11 removed the Java EE and CORBA modules (JEP 320), and there’s no flag to bring them back. Code that still uses them fails with NoClassDefFoundError, which is why the Stack Overflow question on JAXBException is so popular.

Each removal has its own fix:

JAXB and JAX-WS: add them back as dependencies, such as javax.xml.bind:jaxb-api plus the com.sun.xml.bind runtime JEP 320 names. Jakarta XML Binding 3.0 and later renames the package to jakarta.xml.bind, which means changing imports.

JAXB used only for Base64: switch to java.util.Base64. It also replaces sun.misc.BASE64Encoder, which JDK 9 removed.

Nashorn: gone since JDK 15 (JEP 372). javax.script stays, so you’ll need to bundle a JavaScript engine yourself.

Pack200 and RMI Activation: removed in JDK 14 and 17. Pack200’s JEP points you to jlink or jpackage.

JavaFX: Oracle JDK stopped bundling it in JDK 11, so pull it from OpenJFX.

The Applet API and Security Manager still work on 17 but are deprecated for removal. Later JDKs removed or disabled both, so plan your way off them now.

You’re done when the build compiles on 17 and jdeprscan comes back clean.

3. Fix Strong Encapsulation Errors From the Module System

The module system arrived in JDK 9, but you don’t need a module-info.java to run on 17. Classpath code just loads into the unnamed module.

Access to JDK internals is where 17 gets strict. JDK 9 through 15 allowed illegal reflective access by default, with a warning. Then JDK 16 switched the default to deny (JEP 396). JDK 17 made –illegal-access obsolete, so it’s accepted but does nothing beyond a warning (JEP 403).

So an app that ran on 11 with warnings can fail on 17, as in this example from JEP 403:

java.lang.reflect.InaccessibleObjectException: Unable to make field private
java.security.KeyStoreSpi java.security.KeyStore.keyStoreSpi accessible:
module java.base does not “opens java.security” to unnamed module @6e2c634b

Fix these in order:

Upgrade the library doing the reflection. JJWT, for example, fixed its Java 17 errors in 0.12.2, with “no more –add-opens”.

Open only the package the error names if there’s no fix yet. For the error above, that’s –add-opens java.base/java.security=ALL-UNNAMED. Executable JARs can use the Add-Opens manifest attribute.

Copy the same flags into your test JVM. Surefire and Failsafe need them in argLine, or tests can fail while the app runs fine.

sun.misc.Unsafe is still reachable through the jdk.unsupported module, so libraries that only use it keep working. Our Java 17 glossary entry lists the other removals and deprecations.

You’re done when the app passes its tests with a short, documented –add-opens list, ideally empty.

4. Update GC Settings and JVM Flags

If you’ve never picked a garbage collector, the upgrade picks one for you. JDK 8 used the Parallel collector on server-class machines, and JDK 9 made G1 the default (JEP 248). Netflix credits improvements in G1 for its 20% CPU gain. But if a batch job leaned on Parallel’s throughput, set -XX:+UseParallelGC and compare.

Old startup flags are the bigger risk, because JDK 17 won’t start with some of them. You’ll get Unrecognized VM option for these:

-XX:+UseConcMarkSweepGC: CMS was removed in JDK 14 (JEP 363). Switch to G1, or test ZGC, production-ready since JDK 15.

-XX:+PrintGCDetails and -Xloggc still work on 17, with a deprecation warning. Log rotation needs the unified logging syntax:

-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10M

Check your GC dashboards too, since Oracle warns that GC log parsers “will probably need to change.”

You’re done when the JVM boots on 17 without option errors and your dashboards read the new logs.

5. Decide Whether to Switch JDK Distribution at the Same Time

Licensing usually forces the question. Oracle JDK 17 updates from 17.0.13 onward ship under the OTN license, which is free for personal and development use. For production use of those updates, you’ll typically need a paid Oracle subscription, and the Java SE Universal Subscription is priced per employee.

We’d switch vendors in a separate step, on the same Java version. That way, if something breaks, you’ll know which change caused it.

Still on Oracle JDK 8? Switch to another OpenJDK 8 build at the same update level first, then upgrade to that vendor’s 17. Our Deputy CTO, Simon Ritter, demos it in a short tutorial.

Already on OpenJDK, or partly on 17? Pick the 17 build you’ll ship now and test on it from here on.

Leaving Oracle JDK 8 also means checking its proprietary extras and removing -XX:+UnlockCommercialFeatures, which OpenJDK won’t start with. Re-import any private CA certificates into the new JDK’s trust store, too.

Azul Core is a drop-in replacement for Oracle Java SE, built on TCK-verified Zulu builds of OpenJDK. It covers 8 and 17 side by side, so both stages run on one vendor’s builds. Patch level matters too, since CleverTap hit a native memory bug on 17.0.1 that 17.0.6 fixed. We ship quarterly security-only CPUs and monthly security-only CSPUs, plus out-of-cycle fixes for zero-day and critical issues.

Azul Core also costs up to 70% less than Oracle Java’s server subscription pricing. Our OpenJDK migration hub has free templates for planning the switch.

You’re done when you know which 17 build you’ll ship, and every waiting Java 8 service has a supported build.

6. Test on the Build You’ll Ship, Then Roll Out in Stages

Reflection errors, removed flags, and GC changes only appear at runtime, so test in this order:

Run your Java 8 artifacts on JDK 17 before recompiling: Oracle’s guide recommends it, and it separates runtime problems from compile problems. Services that still ship on 8 can compile with –release 8 on the new JDK, as LinkedIn did for Java 11.

Then compile with –release 17 and run the full suite, using maven.compiler.release or a Gradle toolchain.

Check behavior the compiler won’t catch: The UK short name for September became “Sept” in JDK 16 (JDK-8256837), so hard-coded date strings in tests break. Code that parses java.version as “1.8” breaks too, and TLS 1.0 and 1.1 are now off by default, as in recent Java 8 updates.

Compare behavior under production-like load: Capillary mirrored live traffic to Java 17 instances and compared the responses with Java 8. Watch container memory as well as heap.

Roll out in waves: Canary one service and keep a Java 8 image for rollback. Once 17 runs cleanly in production, start the next wave.

You’re done when every service runs on 17 with no InaccessibleObjectException or Unrecognized VM option in the logs.

Where to Go From Here if You’re Halfway There

If you’re mid-upgrade, clear flags and encapsulation errors one service at a time. Keep the rest on a supported Java 8 build until it’s their turn, so each failure stays small.

If you’re leaving Oracle JDK as well, Azul Core gives you supported Java 8 and 17 builds from one vendor. OpenJDK Migration for Dummies and our migration docs walk through the switch step by step.

FAQs

1. Can You Migrate Directly From Java 8 to Java 17?

Yes. JDK 17 runs Java 8 bytecode, so you don’t need to ship 11 in between. It’s a bit more work, though. Michael Roeschter, one of our sales engineers, estimates about 20% more effort for 8 to 17 than for 8 to 11.

2. Do You Need to Rewrite Your Code to Use Java 17 Features?

No. Java 8 code compiles and runs on 17 without records, sealed classes, or any other new feature. We’d hold off, and Roeschter recommends sticking to what Java 11 provides until the app runs as expected.

3. Which Java Version Is Better, 17 or 21?

Java 21, because it’s supported longer. Java 25 is the newest LTS and runs longer still. We support 17 until September 2029, 21 until September 2031, and 25 until September 2033. Netflix found that once you’re on 17, upgrading to Java 21 is “almost a no-op,” so finishing 17 first won’t box you in.

4. Why Upgrade From Java 8 to 17?

Because your frameworks are leaving 8 behind. Spring Boot 3.0 has a Java 17 baseline, and commercial support for Spring Boot 2.7 ends at the end of 2026.