From this month, Oracle releases its JDK 21 updates under a license that is not free in production, and teams that finished a Java 21 migration a year or two ago are asking whether 25 deserves another one. It does. For a team on 21 it is a small upgrade with runtime gains that need no code changes; for a team on 8, 11 or 17, the target is 25, not 21.

Java 21 vs 25 at a glance

Java 21 (September 2023) and Java 25 (September 2025) are consecutive long-term support releases. Between them sit three feature releases, 22, 23 and 24, and a move from 21 to 25 picks up everything they delivered. The table compares the Java 21 new features with the Java 25 features by area.

AreaJava 21Java 25
ConcurrencyVirtual threads final; pinning inside synchronized blocksVirtual threads no longer pin in synchronized; scoped values final; structured concurrency still in preview
LanguageRecord patterns and pattern matching for switch finalUnnamed variables, flexible constructor bodies, module imports, compact source files and instance main methods final
LibrariesSequenced collections, Key Encapsulation Mechanism APIStream gatherers, Class-File API, Foreign Function and Memory API, Key Derivation Function API, ML-KEM and ML-DSA
Memory and startupGenerational ZGC as an optionCompact object headers as a product feature, ahead-of-time cache with method profiles, generational Shenandoah
Garbage collectorsZGC defaults to its non-generational modeZGC is generational only
Removed or restrictedSecurity Manager deprecated for removal; warnings for dynamically loaded agentsSecurity Manager permanently disabled; 32-bit x86 port removed; warnings for JNI and sun.misc.Unsafe memory access
Oracle JDK licenseUpdates under the paid OTN license from October 2026Free updates under Oracle's No-Fee Terms until September 2028

What changed between Java 21 and Java 25, release by release

The four releases after 21 delivered 66 JDK Enhancement Proposals (JEPs), 33 of them final, according to the OpenJDK project pages for JDK 22, 23, 24 and 25. JDK 24 alone delivered 24.

JEPs delivered per release. JDK 21 (LTS, September 2023): 15 JEPs, 8 final, 6 preview, 1 incubator. JDK 22: 12 JEPs, 4 final, 7 preview, 1 incubator. JDK 23: 12 JEPs, 3 final, 8 preview, 1 incubator. JDK 24: 24 JEPs, 14 final, 7 preview, 3 incubator or experimental. JDK 25 (LTS, September 2025): 18 JEPs, 12 final, 4 preview, 2 incubator or experimental. Between the two LTS releases, 66 JEPs, 33 of them final.JEPS DELIVERED PER RELEASE, BY STATUSJDK 21 LTSSep 202315 JEPs, 8 finalJDK 22Mar 202412 JEPs, 4 finalJDK 23Sep 202412 JEPs, 3 finalJDK 24Mar 202524 JEPs, 14 finalJDK 25 LTSSep 202518 JEPs, 12 finalfinalpreviewincubator or experimental
JEPs per release by the status in their titles. Source: OpenJDK project pages for JDK 21 to JDK 25.

The count matters less than what finished. Most preview features of Java 21 went through two to four more rounds and became final in 25. Two did not: structured concurrency is in its fifth preview, and string templates were withdrawn after their second.

Status of each feature in JDK 21 to 25. Virtual threads: final in JDK 21; Foreign Function & Memory API: preview in JDK 21, final in JDK 22; Unnamed variables & patterns: preview in JDK 21, final in JDK 22; Stream gatherers: preview in JDK 22, preview in JDK 23, final in JDK 24; Class-File API: preview in JDK 22, preview in JDK 23, final in JDK 24; Scoped values: preview in JDK 21, preview in JDK 22, preview in JDK 23, preview in JDK 24, final in JDK 25; Compact source files, instance main: preview in JDK 21, preview in JDK 22, preview in JDK 23, preview in JDK 24, final in JDK 25; Flexible constructor bodies: preview in JDK 22, preview in JDK 23, preview in JDK 24, final in JDK 25; Module import declarations: preview in JDK 23, preview in JDK 24, final in JDK 25; Key derivation function API: preview in JDK 24, final in JDK 25; Compact object headers: experimental in JDK 24, final in JDK 25; Generational Shenandoah: experimental in JDK 24, final in JDK 25; Structured concurrency: preview in JDK 21, preview in JDK 22, preview in JDK 23, preview in JDK 24, preview in JDK 25; Primitive types in patterns: preview in JDK 23, preview in JDK 24, preview in JDK 25; Vector API: incubator in JDK 21, incubator in JDK 22, incubator in JDK 23, incubator in JDK 24, incubator in JDK 25; String templates: preview in JDK 21, preview in JDK 22, withdrawn in JDK 23.FEATURE STATUS IN EACH RELEASEJDK 21 LTSJDK 22JDK 23JDK 24JDK 25 LTSVirtual threadsForeign Function & Memory APIUnnamed variables & patternsStream gatherersClass-File APIScoped valuesCompact source files, instance mainFlexible constructor bodiesModule import declarationsKey derivation function APICompact object headersGenerational ShenandoahStructured concurrencyPrimitive types in patternsVector APIString templatesfinalpreviewincubator or experimentalwithdrawn
How the features previewed around Java 21 ended up by Java 25. Source: the JEP pages linked in the sections below.

Preview features need --enable-preview at compile time and at run time, and their API can change between releases, as structured concurrency's did. Code built against a preview in 21 does not run on 25 unchanged, so the features to plan on are the final ones.

Java 21 new features: the baseline you already have

If you run 21 today, these are in production use already, or could be. They carry over to 25 unchanged.

Virtual threads (JEP 444) let a server handle each request on its own thread without a pool of a few hundred OS threads limiting throughput. One line switches an executor over:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    orders.forEach(order -> executor.submit(() -> billing.charge(order)));
}

Our virtual threads guide covers what changes in connection pools, locks and thread-local caches when you do this.

Record patterns and pattern matching for switch (JEP 440, JEP 441) let a switch take a sealed hierarchy apart, and the compiler checks that every case is covered:

sealed interface Payment permits Card, Transfer, Voucher {}
record Card(String last4, Money amount) implements Payment {}
record Transfer(String iban, Money amount) implements Payment {}
record Voucher(String code) implements Payment {}

Money fee(Payment p) {
    return switch (p) {
        case Card(var last4, var amount) -> amount.percent(1.4);
        case Transfer(var iban, var amount) -> Money.of("0.20");
        case Voucher v -> Money.ZERO;
    };
}

Add a fourth payment type and the switch stops compiling until it handles it, which is the point.

Sequenced collections (JEP 431) give lists, deques, sorted sets and linked hash maps one interface for the first and last element and a reversed view: list.getFirst(), list.getLast(), list.reversed(). Before 21, the last element of a list was list.get(list.size() - 1).

Generational ZGC (JEP 439) shipped in 21 as an option behind -XX:+ZGenerational. It became ZGC's default mode in 23 (JEP 474) and its only mode in 24 (JEP 490), so on 25 the flag is obsolete and -XX:+UseZGC always means the generational collector.

Java 25 features for everyday code

These became final between 22 and 25. None of them forces a rewrite; each one shortens code as it is touched for other reasons.

Unnamed variables and patterns

Final since 22 (JEP 456). An underscore marks a variable or pattern component the code never reads, so the voucher case above no longer needs a name for a value it ignores, and neither does a caught exception that is only counted:

case Transfer(_, var amount) -> Money.of("0.20");

try { Integer.parseInt(raw); } catch (NumberFormatException _) { rejected++; }

Flexible constructor bodies

Final in 25 (JEP 513). Statements can now run before super(...) or this(...), so a constructor can validate its arguments before the superclass does any work with them:

class Invoice extends Document {
    Invoice(String number, List<Line> lines) {
        if (lines.isEmpty()) throw new IllegalArgumentException("empty invoice " + number);
        super(number);
        this.lines = List.copyOf(lines);
    }
}

Before 25 the check had to live in a static helper called inside the super(...) argument list.

Stream gatherers

Final in 24 (JEP 485). A gatherer is a custom intermediate operation for a stream, the counterpart of a collector at the end of one. The built-in ones cover windows and running folds:

List<List<Reading>> batches = readings.stream()
        .gather(Gatherers.windowFixed(500))
        .toList();

Batching a stream for bulk inserts used to need a custom spliterator or a detour through a list.

Module imports and compact source files

import module java.base; imports every package a module exports in one line (JEP 511). Compact source files with instance main methods (JEP 512) let a single-file program skip the class declaration and public static void main(String[] args):

void main() throws IOException {
    var rows = Files.readAllLines(Path.of("orders.csv"));
    IO.println("Read " + rows.size() + " rows");
}

The JEP aims these at beginners. In a server codebase they are most useful for small scripts and one-off tools that used to be written in another language because Java needed too much ceremony. A compact source file imports the whole java.base module automatically, which is why the example needs no import lines.

Foreign Function and Memory API

Final since 22 (JEP 454). It calls native libraries and manages off-heap memory without JNI, with bounds and lifetime checks that JNI never had. For most business systems it matters indirectly: libraries that wrapped native code with JNI or sun.misc.Unsafe are moving to it, and Java 24 started warning about both of the old routes.

Structured concurrency in Java: from JEP 453 to JEP 505

Structured concurrency treats a group of related tasks running in different threads as one unit of work: they start together, the parent waits for all of them, and when one fails the others are cancelled. It is the piece that makes virtual threads easy to use safely for fan-out calls, and it is still a preview API in 25.

In Java 21 the first preview (JEP 453) used subclasses of StructuredTaskScope for each policy:

// Java 21, preview
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier<Customer> customer = scope.fork(() -> crm.find(id));
    Supplier<List<Order>> orders = scope.fork(() -> orderService.recent(id));
    scope.join().throwIfFailed();
    return new Summary(customer.get(), orders.get());
}

The fifth preview in Java 25 (JEP 505) replaced the public constructors with static factory methods. The zero-argument open() waits for all subtasks to succeed or for any one to fail, and other policies are passed in as a Joiner:

// Java 25, preview
try (var scope = StructuredTaskScope.open()) {
    Subtask<Customer> customer = scope.fork(() -> crm.find(id));
    Subtask<List<Order>> orders = scope.fork(() -> orderService.recent(id));
    scope.join();
    return new Summary(customer.get(), orders.get());
}

The two versions are not source compatible. The JEP page already lists a sixth preview, JEP 525, so we keep structured concurrency out of production code and use plain executors over virtual threads until the API is final.

Scoped values replace thread-local context

Scoped values are final in 25 (JEP 506), after four previews. A scoped value shares immutable data with the methods a thread calls and with child threads in a structured scope, and it is unbound again when the scope ends:

static final ScopedValue<Tenant> TENANT = ScopedValue.newInstance();

void handle(Request request) {
    ScopedValue.where(TENANT, request.tenant())
               .run(() -> invoices.process(request));
}

// anywhere below handle(), in the same thread or a forked subtask:
Tenant tenant = TENANT.get();

A ThreadLocal holding a tenant, a user or a correlation id can be set and changed anywhere and leaks if nobody removes it. With a million virtual threads, each carrying its own copy, that cost adds up. A move to virtual threads is the usual moment to replace request context in thread locals with scoped values.

Runtime gains you get without code changes

For a team already on 21, these Java 25 features are the main reason to move: they need a JVM option or nothing at all. Our Java end of life guide covers the measured results in more detail; the short version follows.

  • Virtual threads without pinning. From 24, a virtual thread that blocks inside a synchronized block releases its carrier thread (JEP 491). The JEP says this eliminates "nearly all cases" of pinning, the most common reason teams backed off virtual threads on 21.
  • Compact object headers. Headers shrink from 96 to 128 bits down to 64 bits (JEP 450), a product feature in 25 behind -XX:+UseCompactObjectHeaders (JEP 519). In the JEP's measurements, SPECjbb2015 used 22% less heap and 8% less CPU time in one setting, and Amazon tested the feature in hundreds of production services, most of them on backports to 17 and 21.
  • Ahead-of-time cache. A training run records the classes an application loads and links, and later runs start from the cache (JEP 483). In the JEP's example, Spring PetClinic started in 4.486 seconds on JDK 23 and 2.604 seconds on JDK 24 with the cache, 42% faster. Java 25 creates the cache with one option, -XX:AOTCacheOutput (JEP 514), and stores method profiles in it, so the JIT compiler starts from a warm profile (JEP 515).
  • Generational Shenandoah is a product feature in 25 (JEP 521), for teams that run Shenandoah rather than G1 or ZGC.
  • JDK Flight Recorder gained method timing and tracing that can be switched on without code changes (JEP 520), and experimental CPU-time profiling on Linux (JEP 509).

The startup work matters most for services that scale out under load or restart often in containers. The memory work matters for any heap full of small objects, which describes most business systems.

What Java 25 removes or restricts

An upgrade from 21 also meets a short list of changes that cost work:

ChangeReleaseWho it affects
Security Manager permanently disabled (JEP 486)24Applications and libraries that install a security manager; the call now fails
Non-generational ZGC removed (JEP 490)24Startup scripts that pass -XX:-ZGenerational
Warnings on JNI use (JEP 472) and sun.misc.Unsafe memory access (JEP 498)24Libraries with native code or Unsafe; check that logs and tests tolerate the warnings, and plan library upgrades
Windows 32-bit x86 port removed (JEP 479), then the whole 32-bit x86 port (JEP 503)24, 25Anything still shipped as a 32-bit build
String templates withdrawn (JEP 465)23Code written against the preview in 21 or 22
Warnings when agents are loaded into a running JVM (JEP 451)21Monitoring and mocking tools that attach at run time; load them with -javaagent instead

In our upgrades, the JDK change itself is rarely what takes the time. The libraries and the framework do.

Java 17 vs 21, and Java 8 vs 17, for teams further behind

A team on 8, 11 or 17 moving to 25 gets every change above plus everything that came before 21. The language changes since Java 8 add up to a different way of writing Java:

SinceFeatureJEP
10var for local variablesJEP 286
14Switch expressionsJEP 361
14Helpful NullPointerExceptions that name the null variableJEP 358
15Text blocks for multi-line stringsJEP 378
16RecordsJEP 395
16Pattern matching for instanceofJEP 394
17Sealed classesJEP 409
17Strong encapsulation of JDK internalsJEP 403
21Virtual threads, record patterns, pattern matching for switch, sequenced collectionsJEP 444, JEP 440, JEP 441, JEP 431

Java 8 vs 17 is the larger jump, and most of its cost is not in the table. Java 9 introduced modules, and Java 17 enforced strong encapsulation of JDK internals, so libraries that reached into sun.* classes through reflection fail at run time until they are upgraded. Java 11 removed the Java EE modules, JAXB and JAX-WS among them, from the JDK (JEP 320), so they come back as dependencies. Frameworks follow: Spring Framework 6 and Spring Boot 3 need Java 17, and moving to them means the javax to jakarta package rename.

Java 17 vs 21 is a much smaller step in effort: few removals, and the gains are virtual threads, the pattern matching features and generational ZGC. For a team on 17 today, though, stopping at 21 makes little sense: Oracle JDK 21 updates already need a paid license, free builds of 21 end earlier than those of 25, and 25 carries the same code changes plus four more releases of runtime work.

Java support roadmap: how long 8, 11, 17, 21 and 25 get updates

Every LTS release is available from several vendors, and each sets its own end dates. Some publish free builds; others sell support. The chart shows the longest each version stays covered, as of October 2026.

How long each Java LTS release gets updates. Java 8: free builds from Eclipse Temurin and Amazon Corretto to December 2030, paid Red Hat extended support to December 2032. Java 11: Corretto and Oracle and Azul paid support to January 2032. Java 17: Corretto to October 2029, Oracle and Azul to September 2029. Java 21: Corretto to October 2030, Oracle and Azul paid support to September 2031. Java 25: Corretto to October 2032, Oracle and Azul paid support to September 2033.JAVA LTS SUPPORT, LONGEST AVAILABLE20142016201820202022202420262028203020322034October 2026Java 8Temurin, Corretto: Dec 2030; Red Hat ELS: Dec 2032Java 11Corretto: Jan 2032Java 17Corretto: Oct 2029Java 21Corretto: Oct 2030; Oracle, Azul: Sep 2031Java 25Corretto: Oct 2032; Oracle, Azul: Sep 2033releasefree OpenJDK buildspaid support only
Longest support available for each Java LTS release. Sources: Eclipse Temurin, Amazon Corretto, Red Hat, Azul and Oracle roadmaps, read on 14 October 2026.
VendorJava 8Java 11Java 17Java 21Java 25
Oracle, extended support (paid)Dec 2030Jan 2032Sep 2029Sep 2031Sep 2033
Eclipse Temurin (free, "at least")Dec 2030Oct 2027Oct 2027Dec 2029Sep 2031
Amazon Corretto (free)Dec 2030Jan 2032Oct 2029Oct 2030Oct 2032
Microsoft Build of OpenJDK (free, earliest end)no buildSep 2027Sep 2027Sep 2028Sep 2030
Red Hat, end of ELS-1 (paid)Dec 2032Jan 2032Jan 2029Sep 2031Sep 2033
Azul (paid)Dec 2030Jan 2032Sep 2029Sep 2031Sep 2033

Two things stand out. Temurin and Microsoft commit to free builds of 11 and 17 only until 2027, so for teams on those builds the deadline is next year, not 2029. And Java 25 is the only LTS release whose free builds from Temurin, Corretto and Microsoft all run to 2030 or later.

Oracle's own JDK has a separate clock. From the October 2026 update, Oracle JDK 21 updates come under the Java SE OTN license, which needs a paid subscription for production use, and Oracle JDK 25 updates stay free until September 2028 (Oracle roadmap, updated 15 September 2026). How the subscription is priced, and the other ways out, are in our Java end of life dates post.

Is Java modernization worth it? When to upgrade

Most teams are already moving. In JetBrains' 2025 survey, 40% of Java developers used Java 21 regularly, up from 28% a year earlier, and Java 8 fell from 44% to 31%. JetBrains sells Java tools, and respondents could name several versions, so the shares overlap; the direction is the useful part.

Share of Java developers who said they use each version regularly in JetBrains' 2025 survey, more than one answer allowed: Java 21 40% (28% in 2024), Java 17 39% (46% in 2024), Java 8 31% (44% in 2024), Java 11 17% (25% in 2024).DEVELOPERS WHO USE EACH VERSION REGULARLY, 20250%10%20%30%40%50%Java 2140% (28% in 2024)Java 1739% (46%)Java 831% (44%)Java 1117% (25%)
Share of Java developers who use each version regularly; more than one answer allowed. Source: JetBrains, State of Java 2025, more than 5,000 respondents.

Whether an upgrade pays for itself depends on where you start and what the system has to do next. This is the table we use at the start of an engagement:

You runMove toSize of the jobWhat takes the time
Java 2125, within a yearSmall: mostly testingLibrary updates for the JNI and Unsafe warnings, a test run, JVM option changes
Java 1725, skipping 21ModerateSpring Boot 3.x to 4 if you use Spring, library upgrades, tests on concurrency code
Java 1125Larger: framework major versionsSpring 5 to 6 or 7 and the jakarta rename, abandoned libraries
Java 825, in planned stepsA project, priced per applicationRemoved Java EE modules, reflection into JDK internals, every framework major version at once

Every row assumes a test suite that covers the flows the business relies on. Without one, writing it comes first, and it can take longer than the upgrade itself.

The framework sets most of the order. Spring Boot 4 requires at least Java 17 and is compatible up to Java 26, and Jakarta EE 11 also needs Java 17 or later. A system on Spring Boot 2.7 therefore moves Java and the framework together, and our Spring Boot 4 migration guide covers that order of work.

Our most recent example is an online store and back-office ERP, built up over fifteen years, that we moved from Java 17 and Spring 5.3 to Java 25 and Spring 6.2 in about four weeks, with no rewrite and no feature freeze. The work cut the dependency tree's critical and high-severity advisories from 47 to 0. Most of the time went into the framework and abandoned libraries, and the regression tests we added first caught two breaks before release (case study).

So for most teams the Java 21 vs 25 question is one of timing. When is the upgrade not worth it? When the application is due to be retired within a year and a supported OpenJDK build of its current version covers that year. Everything else is a question of order, not of whether.

If you want that order written down for your systems, our Application Modernization Sprint starts with a thirty-minute scoping call and a written inventory of versions, support dates and the upgrade path for each application, with a ballpark estimate.