JDK Upgrade Impact
Purpose
Turn "we are moving to a newer JDK" into a list of things that will break, found deliberately rather than in production, and a measured statement of what the move actually bought.
Two failures this prevents. The first is discovering the breakage at deploy: most of it is findable before deployment by running the existing artefact on the new runtime with warnings made visible. The second is the upgrade credited with a speedup nobody measured — a JDK move usually changes several things at once, which is exactly the condition under which coincidence is mistaken for cause.
Workflow
Inspect exact source/target vendor builds, compiler --release, Maven/Gradle toolchains,
CI/runtime images, dependency/agent support and deployment constraints. The concrete examples
cover JDK 17–25; they do not authorize another upgrade, preview use or unrelated dependency
changes. A JEP's delivery/target release does not establish what is active in the deployed
build. Return observed failures, fixes and checks, unresolved compatibility evidence, and
the measured result or the explicit absence of a performance baseline.
- State both versions and the reason. "Security support ends", "we want compact object headers", "the vendor image moved" are different reasons with different success criteria. An upgrade with no stated reason has no way to be judged finished.
- Run the existing artefact on the new JDK before changing a line. Same jar, same flags, new runtime, in an isolated compatibility environment. Exercise lazy paths and rebuild separately; successful startup does not cover runtime-only or compiler/toolchain changes.
- Make the warnings impossible to miss. Compatibility warnings can occur at startup or
when the affected operation first runs, and are routinely lost in container logs. Capture them — see
references/verification-and-rollout.md. - Classify each failure into one of five kinds using
references/breakage-classes.md: a retired flag, strong encapsulation, a removed or changed API, a changed default, or a third-party agent or library that reads bytecode. The five have different fixes and very different costs. - Fix in reversible order: flags first (cheap, isolated), then dependency upgrades, then your own source. Resist fixing anything that is not broken.
- Measure what the upgrade was justified by, with the method that produced the pre-upgrade baseline. Change one variable: do not tune the collector, the heap or the flags in the same change.
- Stage the rollout so that "it started" and "it is correct under load" are separate gates, and so a rollback is a deploy rather than a project.
Rules
- Compiling is not the test.
--releaseconstrains language, class-file version and the documented platform API for that release; it does not validate third-party binaries or what the runtime encapsulates, removes or refuses at startup. A green build on the new JDK proves very little — and a degraded API proves nothing at all:Thread.stop()(JDK 20),Subject.getSubject(23) andSystem.setSecurityManager(24) still compile and throwUnsupportedOperationExceptionwhen reached (forSubject.getSubject, JDK 23's default disallows the Security Manager; JDK 24 disables it permanently). Exercise those paths. The release-by-release list is inreferences/removed-and-degraded-apis.md. - A JVM that refuses to start is the good case. It is loud, immediate and unambiguous. The
expensive failures are the ones that start: an ignored flag whose value silently no longer
applies, and a changed default that only shows under load. Two changed defaults produce no
dedicated migration warning: from JDK 23
javacno longer implicitly discovers annotation processors solely from the classpath without explicit processing configuration (generated code can be missing, seen as compile errors or aNoSuchMethodError), and from JDK 20 CLDR 42 puts a NARROW NO-BREAK SPACE beforeAM/PMinen_US, breaking any assertion or parser written for a plain space. -Djava.security.manager=allowstops the JVM from starting on JDK 24 and later. Executed on Temurin 25.0.3:java.lang.Error: A command line option has attempted to allow or enable the Security Managerduring VM initialisation. It became permanently disabled in JEP 486 (JDK 24). This is a system property, so it hides in start scripts and Dockerfiles rather than in code.--illegal-accesshas done nothing since JDK 17 (JEP 403). Measured on 25.0.3, it starts and printsIgnoring option --illegal-access=permit; support was removed in 17.0. A team that believes it is holding the door open is not — whatever still works, works for another reason.--add-opensis a migration lever, not a fix. It buys time for a dependency that has not caught up. Each one should have an owner and a reason recorded, because the set only ever grows otherwise, and a build that needs a dozen of them has an upgrade problem it has not addressed.- Find
sun.misc.Unsafebefore it finds you. The memory-access methods were deprecated for removal in JEP 471 (JDK 23) and warn on first use from JEP 498 (JDK 24). Run with--sun-misc-unsafe-memory-access=denyin a test environment: it turns a warning you will ignore into a failure you cannot. - Check third-party bytecode support early. Instrumentation agents, mocking frameworks, generators and proxy libraries may reject unsupported class-file versions when loaded or exercised. Use documented compatible versions; separate upgrades when they also support the old JDK, otherwise record the unavoidable combined change.
- Preview-dependent class files are version-locked. A class marked with minor version
65535 requires the matching Java release and runtime
--enable-preview. Merely supplying the compiler flag does not mark every class as preview-dependent. Recompile and review actual preview usage on the target; do not assume source compatibility. - Retired flags are their own subject. The three states — deprecated, obsolete, expired — and
which release each flag entered them in belong to
jvm-performance-review; that skill's lifecycle matrix is the reference to run the command line against. - The command line you audit is not the whole command line.
JDK_JAVA_OPTIONS,JAVA_TOOL_OPTIONS,@argfiles,-XX:VMOptionsFileand the executable-jar manifest (Add-Opens,Enable-Native-Access, honoured only underjava -jar) all contribute.jcmd <pid> VM.flags -all,VM.command_lineandVM.system_propertiessupply different evidence, not a complete launch/module-access inventory; inspect the contributing files and wrapper configuration too. Protect secrets in this output. - Do not carry a performance claim across the boundary. Any number measured on the old JDK is a number about the old JDK, including your own baselines and any threshold in CI.
- Regenerate class-data/AOT artifacts for the target build. Compatibility checks and launch mode can reject an archive, fall back or fail startup. Verify actual use and diagnostics; silent fallback is not guaranteed. Preserve the old image and matching artifacts for rollback.
References
- Breakage classes — the five kinds of failure, the diagnostic that identifies each, and the fix with its reversibility. Read once something fails on the new runtime.
- Removed and degraded APIs — the release-by-release
table from JDK 17 to 25 of what was removed, degraded to
UnsupportedOperationException, deprecated or changed by default, with the message each produces; the class-file major version per JDK; where flags hide outside the visible command line; and multi-release jars. Read when placing a failure in a release, when a tool reports an unsupported class file version, or when behaviour changed with "no dependency changed". - Verification and rollout — the compatibility pass, making warnings visible, what to measure and against what baseline, and staging the rollout. Read before the first run on the new JDK.