A Java 8 service starts on Java 21 in a test environment, but a scheduled export later produces unreadable customer names. Another path fails only when a library uses reflection. A successful startup is useful evidence. It does not establish that the application can run its real workload correctly.
A Java 8 to Java 21 migration should make runtime differences visible before production changes. Keep the first pass narrow. Establish how the existing application behaves, identify incompatible dependencies and test the target runtime. Leave optional rewrites and new language features for a separate change.
Build an inventory of the application that actually runs
Record the JDK distribution and build used in production, the startup command, environment variables, runtime image, operating system and deployed artifact. Include application servers, database drivers, monitoring agents, build plugins, test tools and scheduled jobs. Inspect deployment scripts as well as source code.
Write down the target distribution and patch level instead of treating Java 21 as one interchangeable installation. Check its vendor documentation for your deployment and support requirements. This guide focuses on application compatibility, not a claim that a particular JDK distribution or license is right for every organization.
Collect representative inputs and expected outputs for critical business paths. Include text files, API responses, batch jobs and external integrations. Store a baseline for errors, latency and resource usage using the same workload you will run against the target.
Try existing binaries before changing the compiler
Oracle's migration guide recommends running the existing program on the target JDK before recompiling. Do this in an isolated environment with production-like configuration. Record startup failures and warnings, then exercise real application paths rather than stopping at a health check.
This separates runtime compatibility from source compilation. If the existing artifact fails on Java 21, you have a concrete problem to classify. If it runs, continue reviewing dependencies and tests. You still need to prove that your build can reproduce a working artifact and that business behavior remains correct.
| Check | What it establishes | What remains |
|---|---|---|
| Existing artifact starts on Java 21 | The tested startup path can run | Background work, integrations and behavior |
| Source compiles with the target toolchain | The tested source and build configuration compile | Runtime compatibility and output correctness |
| Static dependency scan passes | No reported issue in the scanned static dependencies | Reflection and dynamically loaded code |
| Business tests match the baseline | The tested workload behaves as expected | Operational readiness and controlled release |
Update libraries and build tools deliberately
Check compatibility documentation for each third-party tool and library. Oracle calls out the need to update dependencies and to use Maven or Gradle versions that support the target JDK. Do not assume that updating the application compiler also updates the tool that launches it.
Include test runners, code generation, instrumentation and packaging in this review. Keep dependency changes grouped by purpose so failures can be traced. If a framework upgrade requires application changes, make the required scope explicit rather than hiding it inside a generic Java version bump.
Use a repeatable build and retain the dependency list for each tested artifact. Verify the JDK used by local development, continuous integration and the deployed process independently. A build passing on one runtime does not establish which runtime the service uses after deployment.
Find removed components and internal API access
Run jdeps on your application and relevant libraries to identify static dependencies, including internal JDK APIs. Review the findings and supported replacements. jdeps is a static analysis tool, so it does not warn about every dependency reached through reflection. Exercise those paths in runtime tests.
Check for components that were bundled with Java 8 but later removed. The Java EE and CORBA modules were removed in JDK 11, including the modules for JAXB and JAX-WS. Applications using them need explicit dependency and deployment decisions. The Nashorn JavaScript engine and its tools were removed in JDK 15. Inventory embedded scripts before deciding on a replacement.
Inspect startup flags too. The CMS garbage collector was removed in JDK 14. Copying an old startup command unchanged can fail before the application starts. Review each option against the target JDK rather than deleting unexplained flags until the process happens to launch.
Treat reflective-access workarounds as tracked exceptions
JDK 17 and later strongly encapsulate JDK internals. Code that reflectively accesses non-public fields and methods of Java APIs can throw InaccessibleObjectException. The old illegal-access launcher option is obsolete and no longer restores the earlier permissive behavior.
Prefer library versions that use supported APIs. When an immediate upgrade is not possible, carefully reviewed add-opens or add-exports options can grant specific access. Record the affected library, exact access requirement, owner and removal condition. Avoid granting broad access simply because a copied command made a test pass.
Test text encoding, formatting and runtime layout
JDK 18 and later use UTF-8 as the default charset for Java SE APIs. Earlier defaults depended on the runtime environment. Test files that rely on implicit encoding, especially imports and exports shared with older systems. Use fixtures containing the real character sets your business processes handle.
Oracle also recommends checking behavior such as date and currency formatting. Compare expected values and file content, not only whether a job reports success. Where an interface requires a particular encoding or format, make that requirement explicit and test it at the boundary.
Review scripts that assume the old JDK layout. Modern JDKs no longer provide rt.jar and tools.jar in the old form. The application class loader is no longer a URLClassLoader. Custom loading, inspection and packaging tools may rely on those details, so include them in the compatibility tests.
Make the production cutover a separate decision
- Produce a reproducible artifact and target runtime configuration.
- Run critical requests, background jobs and integration tests against the baseline.
- Compare output correctness, errors and operational measurements under representative load.
- Document accepted differences and unresolved blockers with named owners.
- Plan a limited initial release and define the conditions that stop expansion.
- Keep the previous artifact and runtime configuration available, and rehearse recovery.
Include data compatibility in the recovery plan. If the release changes persisted formats or database structures, reverting only the runtime may not restore the previous application. Keep those changes separate when possible and test recovery against the actual state they produce.
How Remqa supports Java migration work
Remqa's code migration work can cover dependency inventory, compatibility fixes, business tests and deployment checks. AI-assisted edits should be tied to a specific failure and reviewed against the application's expected behavior. A broad automatic rewrite adds uncertainty when the immediate goal is a verified runtime move.
Bring the build configuration, startup command and one representative failing path to an initial discussion. For projects that also move stored information, coordinate the work with a data migration plan. Start with the evidence needed to approve the change, then scope the engineering around it.