Oracle JDK to OpenJDK: A Guide to Reliable Migration Testing
TL;DR
One of the most common infrastructure changes Java developers and operators are dealing with today is the migration from Oracle Java to OpenJDK. The reason is the licensing changes made by Oracle and the maturity of the OpenJDK distributions. The migration process is quite simple: replace the JDK, recompile the code, and redeploy the application. However, the differences between the two runtimes can lead to unexpected issues that are not caught by unit tests. In this post, you will learn how to use the traffic replay functionality of Speedscale to record the traffic of your Oracle JDK-based Java deployment, replay it against the same application running on OpenJDK, and compare the results to inspect differences on captured paths before release.
What makes this migration more complicated than it seems
Oracle JDK and OpenJDK-based distributions share much of their implementation. Compare the exact vendor, major version, patch level, configuration, and container image; do not attribute a Java 8-to-21 behavior change to the vendor switch alone. Oracle documents specific distribution differences and recommends testing existing applications during migration preparation.

TLS and cryptographic behavior
Check the actual truststore, enabled protocols, disabled algorithms, and provider configuration of both deployments. A vendor name alone does not establish a different provider chain. Java’s provider architecture allows installed providers and their preference order to vary. Test real downstream TLS connections separately from replay against mocks.
Serialization and encoding
Java serialization can behave differently across different JDK versions. If your application uses:
ObjectOutputStream/ObjectInputStreamfor caching or message passing- XML marshalling using JAXB (now removed from JDK in Java 11)
- Customised
toString()output included in API responses - Locale-sensitive string formatting (e.g., number formatting, date formatting)
Then you may experience different byte-level output when using OpenJDK compared to Oracle JDK, leading to parse errors or cache misses.
Garbage collection and performance
Oracle JDK and OpenJDK can have different default garbage collection algorithms depending on their version. Moving from Oracle JDK 8, which defaults to parallel garbage collection, to an OpenJDK 17 or 21 distribution, which defaults to G1 garbage collection, will change your application’s performance characteristics. The latency effect is workload-dependent; measure repeated runs rather than predicting a particular p99 change.
Removed and relocated APIs
If you’re skipping a version of the JDK as part of your migration (e.g., Oracle JDK 8 to OpenJDK 17 or 21), you will encounter removed or relocated APIs:
javax.xml.bind(JAXB) - moved to Jakarta EEjavax.activation- removedjava.corba- removedsun.misc.Unsafe- partially removed- JavaFX - removed, available as a separate project
These will cause compile-time errors, which will be easy to correct, but runtime differences in the replacement libraries will be harder to understand.
Why traffic replay is the right tool for the job
Existing testing methodologies are insufficient for JDK migrations:
- Unit tests test the logic of your code but do not test the runtime behavior of the JDK. If you change the serialization of
java.util.HashMapin a way that changes the order of the map entries, a unit test will only detect this if you specifically write a test for this behavior. - Integration tests are more effective but only cover a limited number of successful scenarios. In reality, the number of edge cases is enormous.
- Manual testing is impossible to scale and still won’t cover the vast number of API interactions a real system experiences in a day.
This is where traffic replay comes in. By capturing the real traffic of a system – i.e., supported recorded API and dependency exchanges – you create reusable cases for the captured paths. And by replaying this traffic against the new OpenJDK version, you can compare observed responses using your configured assertions.

Speedscale makes this process efficient by:
- Recording supported traffic: inbound API calls and outbound database, cache, and service exchanges
- Mocking backend dependencies to replay the traffic without requiring a real environment
- Automatically diffing the response between the original traffic and the replayed traffic
- Integrating into CI/CD – so you can gate your OpenJDK deployment based on passing replay tests
Step-by-Step: Validating Your Migration with Speedscale
Prerequisites
- Kubernetes cluster with your Java application deployed with Oracle JDK
- Speedscale operator installed
speedctlCLI tool installed locally- Your application deployed as Kubernetes Deployment, StatefulSet, or DaemonSet
Step 1: Instrument Your Oracle JDK Deployment for Recording
Install the operator and collector, check the Java-agent compatibility matrix, and enable capture for the workload. Supported JVM capture requires the Java agent, new pods, and a JVM restart. Inspect decoded inbound and outbound exchanges; eBPF alone does not decode every Java TLS transport.
Let this instrumented deployment run under normal production traffic for a representative period. What this means will vary depending on your application:
- API services: Choose representative peak and off-peak windows, then check endpoint and error-case coverage explicitly.
- Batch processors: At least one entire batch cycle.
- Event-driven services: A representative period should be long enough to capture all varieties of events.
- Stateful services: Don’t forget to capture your application startup, as you want to capture cache warming, connection pool initialization, etc.
Supported capture may include HTTP requests, gRPC, and database wire exchanges; JDBC is a client interface, so verify the actual database protocol and TLS path. Each of these interactions will be captured as an RRPair — the smallest unit of traffic.
Step 2: Create a snapshot
After you’ve captured a representative period of production traffic, you’ll want to create a snapshot. A snapshot is a carefully selected set of RRPairs, which will be our test set.
You can create a snapshot from the Speedscale UI by going to Traffic Viewer, filtering on your service and recording time window, and clicking “Create Snapshot”. Or, you can use our CLI:
# Replace cluster and namespace with your source environment.
speedctl create snapshot --name migration-baseline \
--cluster SOURCE_CLUSTER --namespace SOURCE_NAMESPACE \
--service my-java-service --start 30m --end 25m
You should review your snapshot in Traffic Viewer to ensure it has captured all the traffic you expect. Pay close attention to:
- Inbound traffic: This will be our tests. All API calls your service has received will be replayed against our OpenJDK build.
- Outbound traffic: These will serve as your mocks. The database queries, API calls, and cache queries will all be mocked via the Speedscale responder.
LLM prompt for this step
Create a Speedscale snapshot for my real source cluster, namespace, service, and representative time window. Use installed CLI help, verify capture completeness, and retain the snapshot ID for both runtime versions.
Step 3: Build and deploy your OpenJDK image
Next, you’ll build the same application with the OpenJDK. If you’re staying on the same version (for example, from the Oracle JDK 17 to the Eclipse Temurin 17), this is typically just the base image being swapped out:
# Before - Oracle JDK
FROM container-registry.oracle.com/java/jdk:17
# After - Eclipse Temurin / OpenJDK
FROM eclipse-temurin:17-jre-jammy
COPY target/my-java-service.jar /app/my-java-service.jar
ENTRYPOINT ["java", "-jar", "/app/my-java-service.jar"]
If you’re also moving to a different major version (for example, from the Oracle JDK 8 to the OpenJDK 21), you’ll also need to fix up the changes that were made to your code to work with the new version. Once you’ve got that working, you can move on to the replay step.
Deploy your OpenJDK build to your staging or test namespace.
Step 4: Replay the snapshot against your OpenJDK deployment
Replay your Oracle JDK baseline snapshot against your newly created OpenJDK deployment. In the Speedscale UI, navigate to your snapshot and click Replay. Select your OpenJDK test workload as the target, and Speedscale will take care of the rest. The generator will send the recorded inbound traffic to your app, while the responder will mock all the outbound dependencies with the recorded responses.
Alternatively, you can also use the speedctl command to initiate the replay from the command line:
# Use the snapshot ID and reviewed test-configuration ID from Speedscale.
speedctl infra replay --snapshot-id SNAPSHOT_ID \
--cluster TEST_CLUSTER --namespace staging --service my-java-service \
--test-config-id TEST_CONFIG_ID
During the replay process, Speedscale will:
- Deploys a generator pod, which replays all the recorded incoming requests to your OpenJDK application
- Deploys a responder pod, which intercepts all the outgoing calls and returns the recorded responses
- Records the actual responses returned by your OpenJDK application
- Compares these with the expected responses returned by the Oracle JDK recording
LLM prompt for this step
Build the candidate image with the reviewed OpenJDK distribution and version, preserving app configuration and trust settings. Deploy only to the chosen test environment, replay the preserved snapshot with the real test-configuration ID, wait for completion, and compare it with the Oracle JDK baseline.
Step 5: Analyze the replay report
Finally, open the replay report in the Speedscale UI. Here, you can see the following information:
Response correctness: Are the response bodies the same? Speedscale compares the responses field by field, so you can see exactly what changed in the JSON fields, XML elements, or headers.
Some common differences you may see include:
| Issue | Example | Root cause |
|---|---|---|
| Date formatting change | 2026-02-13T19:00:00.000+0000 vs 2026-02-13T19:00:00.000Z | Different SimpleDateFormat timezone handling |
| Header ordering | X-Custom-Header header position changed | HashMap iteration order changed |
| TLS failure | Outbound call to payment gateway returns 0 bytes | Cipher suite mismatch |
| Numeric precision | 0.30000000000000004 vs 0.3 | Float/double serialization difference |
| Missing response field | jaxbElement field missing | JAXB replaced with Jackson but mapping differs |
Latency comparison: Are the response latencies comparable? Compare the p50, p95, and p99 latencies. Changes in GC behavior may cause latency spikes that are not apparent in functional testing. For more on how you can incorporate performance testing into your workflow, see A Developer’s Guide to Continuous Performance Testing.
Error rate: Did the requests that worked with the Oracle JDK fail with the OpenJDK? You can filter the report by status code to see 500s, 502s, and connection timeouts.
Step 6: Fix, rebuild, replay — iterate until clean
For every regression found in the replay report:
- Diagnose: Use the RRPair diff to locate the exact change. Speedscale displays the recorded (expected) response and the replayed (actual) response.
- Fix: Modify the application code or configuration to match the expected behavior. This could be adding a cipher suite configuration, a date format pattern, or replacing a removed API.
- Rebuild: Push a fresh OpenJDK image.
- Replay again: Use the same snapshot to replay the request to the fresh image.
Repeat until the selected response assertions and performance criteria pass, then complete the separate integration and migration checks.
Step 7: Gate your CI/CD pipeline
Now that you’ve validated the migration in the staging environment, you should also enable replay tests in your CI/CD pipeline so that all future changes are continuously validated against the same baseline of production traffic. Check the Speedscale CI/CD integration guide for setup instructions for GitHub Actions, GitLab CI, Jenkins, Azure DevOps, CircleCI, and more.
This checks later changes against the saved cases; new requirements and unrecorded behaviors still need their own tests.
Using proxymock for local validation
Instead of using Speedscale in the cluster, you could also use proxymock to validate the migration before deploying it to the cluster. Check the proxymock page to learn about the alternative. If you choose to use proxymock, check the proxymock getting started guide to learn the details about the setup.
Note: The above command only records inbound traffic. If you are making outbound calls to databases, APIs, etc., you will need to set custom command line arguments or environment variables to capture this. Check out the proxymock docs for proxy config and outbound capture.
Record with the baseline runtime
Install and initialize proxymock, then follow the Java proxy and truststore setup. Use a known application route in the example below:
proxymock admin certs --jks
proxymock record --app-port 8080 --out ./proxymock/migration-baseline \
-- java -jar my-java-service.jar
Send representative requests through port 4143, then stop the recorder and application. Direct JVM launch lets the wrapper set HTTP proxy and truststore properties. Database clients and custom transports may require additional configuration.
LLM prompt for this step
Prepare the local baseline runtime's truststore using proxymock certificate tooling, then record my Java service on port 8080 into proxymock/migration-baseline. Exercise the representative request set through the recorder and retain the exact runtime version and recording.
Replay against the candidate runtime
Select the candidate JDK in the application terminal and start the application with the recorded mocks:
proxymock mock --in ./proxymock/migration-baseline --no-passthrough \
-- java -jar my-java-service.jar
In another terminal, replay the saved inputs against port 8080:
proxymock replay --in ./proxymock/migration-baseline \
--test-against http://localhost:8080 \
--out ./proxymock/results/migration-candidate \
--fail-if 'requests.result-match-pct < 100'
Inspect observed responses and replay-verdict.json. The example gate checks the response assertion match percentage; also configure a baseline/body regression gate for compatibility checks. --no-passthrough controls calls reaching the mock, so enforce routing separately. Mocked TLS responses do not validate negotiation with the actual provider.
LLM prompt for this step
Run the candidate OpenJDK build with dependencies mocked from proxymock/migration-baseline and passthrough disabled. Replay the preserved inbound requests against port 8080, write separate candidate results, enforce the supported mismatch gates, and report behavior and timing differences.
Migration Checklist
Before you consider this migration complete, be sure to:
- All API Endpoints produce identical response bodies, or you’ve documented intentional differences
- Latency p99 is within an acceptable variance of the Oracle JDK baseline
- TLS connections to all downstream services
- Serialization format (JSON, XML, Protobuf) is byte compatible where necessary
- Database queries produce the same results
- Cache hit rates remain the same (watch out for serialization-based cache keys!)
- Scheduled jobs and batch processes work correctly
- JVM flags are correctly mapped to OpenJDK flags (some Oracle flags don’t exist in OpenJDK!)
- Monitoring and APM tools are compatible with the OpenJDK JVM
- Replay tests are integrated into the CI/CD workflow to catch any regressions introduced in subsequent updates
Run this workflow with an LLM
Give this prompt to a coding assistant with access to your project and this article. Supply environment-specific values it cannot discover.
Whole-workflow LLM prompt
Read https://speedscale.com/blog/how-to-migrate-oracle-java-to-openjdk-with-traffic-replay/ and inspect my project instructions first. Validate my Oracle JDK to OpenJDK migration using one preserved workload and explicit runtime versions. Choose either the Kubernetes snapshot path or the local proxymock path, establish a baseline, run the candidate with equivalent dependencies and state, and report response, error, and latency differences before deployment. Check installed tool help before adapting commands. Report the checks actually run, their results, and anything you could not verify.
Conclusion
While migrating from Oracle Java to OpenJDK is a necessary step for many organizations, it doesn’t have to be a leap of faith. Use recorded traffic to check the selected interactions on the candidate runtime. Keep separate checks for real TLS integrations, background jobs, database state, and behaviors absent from the capture.
The record-replay-compare pattern makes the Oracle-to-OpenJDK migration a methodical and iteratable process. And once you’ve made the switch, the same pattern continues to protect you from regressions in every subsequent release. See how Speedscale works or book a demo to validate your migration with production traffic.
Resources
- Speedscale documentation
- proxymock quickstart
- Kubernetes annotations reference
- Speedscale glossary
- proxymock website
Frequently asked questions
Which runtime differences should I test?
Test the exact vendor, JDK version, patch level, and container image you plan to deploy. Check TLS trust and providers, serialization, locale and charset behavior, startup flags, dependencies, and resource use. Separate a vendor switch from a major-version upgrade where possible; a difference caused by changing Java versions is not automatically a vendor incompatibility.
Can I run the comparison locally?
Yes. Run the same application artifact under each selected JDK and replay one reviewed input set against both. proxymock can start a directly invoked JVM with proxy and truststore properties; custom clients and database connections need their own routing. A mocked TLS dependency does not validate negotiation with the real downstream server, so keep that integration check separate.
How do I separate performance noise from a real migration regression?
Keep hardware limits, traffic mix, dependency configuration, data, and warm-up procedure comparable. Repeat both versions, collect enough samples, and examine CPU, garbage collection, errors, and latency percentiles. Give the generator headroom. Report the observed variation and scope; one faster or slower replay is insufficient evidence of a general JDK performance difference.
Sources: JDK migration preparation, Java cryptographic provider configuration, Java with proxymock, Generator sizing and bottlenecks.