in

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.

Timeline showing the features added between Java 21 and Java 25 LTS and the migration steps

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

synchronized blocks no longer pin the carrier thread; virtual threads are safe with old libraries

Scoped Values

25 / JEP 506

The immutable, cheap, virtual-thread-friendly alternative to ThreadLocal; request context, tenant, tracing

Module import declarations

25 / JEP 511

import module java.base; — for scripts and small tools

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 super(); cleaner subclass constructors

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%; -XX:+UseCompactObjectHeaders

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, synchronized blocks 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 --release 25, run all tests; list what breaks

1–3 days

3. Clear the warnings

sun.misc.Unsafe memory-access warnings (JEP 498) → upgrade the library; --enable-native-access=ALL-UNNAMED for JNI/FFM; remove any SecurityManager use

1–5 days

4. Runtime

Move the base image to 25; review JVM flags (try -XX:+UseCompactObjectHeaders under load test); a training run for the AOT cache

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

  1. Old ByteBuddy/Mockito: "Unsupported class file major version 69" in the test phase; the fix is upgrading, -Dnet.bytebuddy.experimental=true is a stopgap.

  2. Libraries using Unsafe: A warning today, an error in future releases; old Netty, Hazelcast, some serializers. Upgrade now.

  3. 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.

  4. 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.

  5. Forgetting the compiler target: If maven.compiler.release stays 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.