The full picture
- Inventory & blockers: scan for internal-API usage (
jdeps --jdk-internals), removed pieces (JAXB/JEE modules, Nashorn, SecurityManager deprecation, CMS), reflection-heavy libs needing upgrades (older Lombok/Mockito/ASM breaks on new class-file versions). - Paved road first: one shared parent/BOM, base Docker images, toolchains in Gradle/Maven so the JDK is code, CI running the test matrix on current + target JDK — surface breakage continuously, not on migration day.
- Behavioral risk spots: GC default changes (8's Parallel → G1), locale/Unicode/CLDR differences,
Thread.stop/finalization deprecations, TLS defaults, and JIT warm-up profile changes — canary + perf baselines per service. - Rollout: pilot 2–3 representative services → template the fixes → batch by team with a deadline and a 'migration is a feature' budget line. Measure: JDK version histogram dashboard; the program is done when it hits zero-on-old.
- Sell the wins: fewer pauses (G1 improvements are free), virtual threads unlock simpler code + higher density, records/sealed shrink boilerplate, and CVE posture — auditors care even when engineers don't.
| Hop | Highlights you actually use |
|---|---|
| 8 → 11 | var, HttpClient, single-file run, G1 default, jlink; lose JEE modules (add explicit deps) |
| 11 → 17 | records, sealed, switch expressions, text blocks, NPE messages, stronger encapsulation of internals |
| 17 → 21 | virtual threads, pattern matching for switch, record patterns, SequencedCollection, generational ZGC |
| 21 → 25 | unnamed variables (_), scoped values, structured concurrency (preview→final track), FFM API replacing JNI use-cases |