Java 25 LTS: What's New and an Enterprise Migration Plan
Java 21 was the big leap with virtual threads; Java 25 LTS is the 'polish' release enterprise projects will stand on for the next four years. The permanent features that arrived between 21 and 25 (Scoped Values, compact object headers, the AOT cache, flexible constructors), the performance gains and a step-by-step migration plan with library compatibility and pitfalls.
Java 21, with virtual threads and pattern matching, was enterprise Java's biggest leap of the past decade. Java 25 LTS (September 2025) is a different kind of release: not a revolution, a polish. Features that matured in 22, 23 and 24 became permanent, JVM startup time and memory use improved visibly, and old baggage (the SecurityManager, 32-bit x86) was dropped. For enterprise projects the question isn't "should we move" but "when and how".
This article covers the LTS-to-LTS logic, the permanent features that arrived between Java 21 and 25 and their enterprise value, the performance gains, a step-by-step migration plan, library compatibility and the pitfalls we see in the field.
Why LTS to LTS?
LTS | Released | Highlights | Status |
|---|---|---|---|
Java 8 | 2014 | Lambdas, Streams | Paid/extended support only; security risk |
Java 11 | 2018 | Module system, HTTP Client | Support ending; migration required |
Java 17 | 2021 | Sealed types, records, text blocks | Widespread; updates until 2029 in many distributions |
Java 21 | 2023 | Virtual threads, pattern matching, sequenced collections | The current enterprise baseline |
Java 25 | 2025 | Scoped Values, AOT, compact object headers | The new baseline; the next LTS in 2027 (Java 29) |
The interim releases every six months (22, 23, 24) get updates only until the next release; enterprise production skips them and moves to an LTS every two years. Delaying the LTS move means losing security patches and library support: libraries raise their baselines (17 for Spring Boot 3, 17 for Spring Framework 7, 21 in the coming generations), and a project stuck on an old JDK stops receiving updates.
Permanent features from 21 to 25
Feature | Release / JEP | Enterprise value |
|---|---|---|
Foreign Function & Memory API | 22 / JEP 454 | Native library calls without JNI; the future of native bridges such as SAP JCo |
Unnamed variables and patterns ( | 22 / JEP 456 | Less noise in catch and pattern blocks |
Stream Gatherers | 24 / JEP 485 | Custom intermediate operations (windowing, grouping) for batch processing |
Class-File API | 24 / JEP 484 | A standard bytecode API instead of ASM; frameworks such as Spring and Hibernate keep up with new releases |
AOT class loading and linking (Leyden) | 24 / JEP 483; 25 / JEP 514, 515 | Startup time drops noticeably; an AOT cache from a training run |
Virtual-thread pinning fix | 24 / JEP 491 |
|
Scoped Values | 25 / JEP 506 | The immutable, cheap, virtual-thread-friendly alternative to ThreadLocal; request context, tenant, tracing |
Module import declarations | 25 / JEP 511 |
|
Compact source files and instance main | 25 / JEP 512 | Teaching and scripting; indirect benefit in enterprise code |
Flexible constructor bodies | 25 / JEP 513 | Validation before |
Key Derivation Function API | 25 / JEP 510 | Standard KDFs such as HKDF; fewer third-party dependencies in crypto code |
Compact object headers | 25 / JEP 519 (opt-in) | Heap shrinks 10–20%; |
Generational ZGC by default, Generational Shenandoah | 23 / JEP 474; 25 / JEP 521 | Low-pause collectors with less CPU and memory |
JFR improvements | 25 / JEP 518, 520 | Method timing and tracing; safer profiling in production |
Removed: the SecurityManager permanently disabled (24 / JEP 486), the 32-bit x86 port (25 / JEP 503), non-generational ZGC (24 / JEP 490). Features still in preview (Structured Concurrency, Stable Values, primitive types in patterns) don't go into production code.
Three features, three examples
// 1) Scoped Values: instead of ThreadLocal for request context — immutable, virtual-thread-friendly, cleaned up automatically
public final class Context {
public static final ScopedValue<TenantId> TENANT = ScopedValue.newInstance();
}
// in the filter
ScopedValue.where(Context.TENANT, tenant).run(() -> chain.doFilter(req, res));
// in any layer
TenantId t = Context.TENANT.get(); // NoSuchElementException outside the scope: a leak is impossible// 2) Stream Gatherers: batch processing in fixed-size windows (e.g. sending to the ERP in batches of 500)
orders.stream()
.gather(Gatherers.windowFixed(500))
.forEach(batch -> erp.sendBatch(batch));// 3) Flexible constructor bodies: validation and field preparation before super()
public class CorporateCustomer extends Customer {
private final TaxNumber taxNumber;
public CorporateCustomer(String name, String taxNumber) {
if (taxNumber == null || taxNumber.length() != 10) throw new IllegalArgumentException("tax number");
this.taxNumber = new TaxNumber(taxNumber); // now allowed before super()
super(name);
}
}Performance: measurable gains
Startup time: The AOT cache (
-XX:AOTCache, produced by a training run) cuts Spring Boot startup one step beyond CDS; scaling and deployment on Kubernetes get faster (see Spring Boot on Kubernetes).Memory: Compact object headers reduce the heap by 10–20% in applications dense with small objects (DTOs, entities, collections); more work in the same container, or a smaller container.
Virtual threads: With the pinning fix,
synchronizedblocks in JDBC drivers and old libraries no longer hurt throughput; teams that gave up on virtual threads in 21 should try again in 25.GC: With generational ZGC, low pauses no longer require huge heaps.
The migration plan, step by step
Step | What's done | Duration (typical) |
|---|---|---|
1. Inventory | Dependency list and their JDK 25-compatible versions: Spring Boot 3.5.x (latest patch) or 4.x, Hibernate 6.6+/7, Jackson, Mockito and ByteBuddy (class-file version 69), Lombok, MapStruct, Testcontainers, JDBC drivers | 1–2 days |
2. Build and test | A second CI job: compile with | 1–3 days |
3. Clear the warnings |
| 1–5 days |
4. Runtime | Move the base image to 25; review JVM flags (try | 2–3 days |
5. Gradual rollout | Test → staging → canary in production; compare GC logs, latency and memory metrics with 21 | 1–2 weeks |
Mandatory code changes are usually none: code compiled on 21 runs on 25. The time goes into library versions and clearing warnings, which is why step 1 is the most critical.
Pitfalls
Old ByteBuddy/Mockito: "Unsupported class file major version 69" in the test phase; the fix is upgrading,
-Dnet.bytebuddy.experimental=trueis a stopgap.Libraries using Unsafe: A warning today, an error in future releases; old Netty, Hazelcast, some serializers. Upgrade now.
Enabling compact headers without testing: A pure gain in most applications, but (rare) libraries that touch the object header through JNI are affected; load-test first.
Straight from Spring Boot 2.x to 25: Not possible; first move to 3.x (the Jakarta namespace), then the JDK. Don't make two big changes in one release.
Forgetting the compiler target: If
maven.compiler.releasestays at 21 in Maven, the language features are unavailable; raise it deliberately.
Frequently asked questions
Can we go straight from Java 17 to 25?
Yes; 17 → 25 is done in one step and brings all the gains in between (virtual threads, pattern matching, record patterns) together. The plan is the same; the inventory step is a bit longer.
Which JDK distribution?
Eclipse Temurin, Amazon Corretto, Azul Zulu, Red Hat's build of OpenJDK and Oracle JDK come from the same OpenJDK source; the differences are support duration, licensing and extra tooling. Temurin is common and trouble-free for container images; if an enterprise support contract is required, choose accordingly.
Is the move required to enable virtual threads?
No, they exist in 21; but the pinning fix in 24 is the change that made virtual threads safe with old libraries. For projects that will use virtual threads seriously, 25 is the right baseline.
Conclusion
Java 25 LTS is the baseline enterprise projects will stand on safely for four years: faster startup, a smaller heap, safer virtual threads, context handling without ThreadLocal and a cleaned-up platform. The migration is not a coding project but an inventory and verification project; with a good CI it is completed in a few weeks.
To move your existing Java applications to 25, build the library inventory and measure the performance gains, see our maintenance and support service.