Java 25 LTS: Every Feature Worth Knowing From Java 21 to 25

Java 25 LTS is the first long-term support release since Java 21, and it packages four feature releases (22, 23, 24, 25) into one upgrade target. Virtual threads no longer pin on synchronized. Scoped values are final. Constructors can run validation before super(). Compact object headers cut heap usage. This guide walks through every change that affects production code, with runnable examples, migration tables, and refactoring patterns.

Quick Takeaways

  • Language: Unnamed variables (_), flexible constructor bodies, module import declarations, and compact source files are all final by Java 25.
  • Concurrency: ScopedValue is final in 25. Virtual threads stop pinning carrier threads on synchronized as of Java 24. Structured concurrency is still in preview.
  • Libraries and runtime: Stream Gatherers, the Class-File API, and the Foreign Function & Memory API are final. Compact object headers and generational Shenandoah are product features in 25.
  • Removals: The Security Manager, 32-bit x86 ports, and non-generational ZGC are gone. Audit these before upgrading.
Area Java 21 LTS Java 25 LTS
Virtual threads Final, but pins on synchronized Final, no synchronized pinning
Pattern matching Record patterns, switch patterns Plus _, primitive patterns (preview)
Context passing ThreadLocal ScopedValue (final)
Startup CDS archives AOT cache (classes, linking, profiling)
GC options G1, ZGC (gen. optional), Shenandoah Generational ZGC only, generational Shenandoah
Entry point public static void main Instance main, compact source files

What Java 21 Already Gave You

Java 21 is the baseline for this comparison, so here is what you already have before moving up.

  • Virtual threads (JEP 444)
  • Record patterns and pattern matching for switch (JEP 440, 441)
  • Sequenced collections (JEP 431)
  • Generational ZGC (JEP 439)

Sequenced collections added getFirst(), getLast(), and reversed() to ordered types.

import java.util.List;

public class SequencedDemo {
    public static void main(String[] args) {
        List<String> versions = List.of("17", "21", "25");

        // Uniform API across List, Deque, LinkedHashSet, LinkedHashMap
        System.out.println(versions.getFirst());   // 17
        System.out.println(versions.getLast());    // 25
        System.out.println(versions.reversed());   // [25, 21, 17]
    }
}

The output shows the first element, the last element, and a reversed view. The view is not a copy, so changes to the source list show through.

Language Features From Java 22 to 25

Unnamed Variables and Patterns (Final in Java 22)

The underscore _ marks a binding you intentionally ignore. It removes noise from record patterns, catch blocks, and lambdas.

sealed interface Shape permits Circle, Rect {}
record Circle(double radius) implements Shape {}
record Rect(double width, double height) implements Shape {}

static double heightOrDiameter(Shape shape) {
    return switch (shape) {
        case Circle(var r)   -> r * 2;
        case Rect(_, var h)  -> h;   // width is intentionally ignored
    };
}

The compiler verifies that the sealed hierarchy is exhaustive, so adding a new Shape subtype breaks compilation here instead of failing at runtime.

Flexible Constructor Bodies (Final in Java 25)

You can now run statements before super(...) or this(...), as long as they don’t read this. Argument validation no longer needs static helper methods.

public class Amount {
    protected final long cents;
    protected Amount(long cents) { this.cents = cents; }
}

public class PositiveAmount extends Amount {
    public PositiveAmount(long cents) {
        // Validation runs before the superclass constructor
        if (cents <= 0) {
            throw new IllegalArgumentException("Amount must be positive: " + cents);
        }
        super(cents);
    }
}

An invalid value fails fast with an IllegalArgumentException, and the superclass is never initialized with bad state.

Module Import Declarations (Final in Java 25)

One line imports every public type exported by a module’s packages.

import module java.base;

public class Report {
    public static void main(String[] args) {
        // List, Map, Stream, Path, and more resolve without separate imports
        Map<String, List<Path>> index = Map.of("logs", List.of(Path.of("app.log")));
        System.out.println(index);
    }
}

Use it in scripts and small tools. In large codebases, explicit imports keep dependencies visible during review.

Compact Source Files and Instance Main (Final in Java 25)

A source file can contain top-level methods and no class declaration. The class is implicit.

void main() {
    IO.println("Hello, Java 25");
}

Run it with java Hello.java. IO.println comes from the new java.io.IO helper, and compact files import it automatically. This mostly benefits teaching and scripting, but it also shortens the path from idea to running code.

Primitive Types in Patterns (Third Preview in Java 25)

JEP 507 lets instanceof and switch use primitive type patterns with safe conversions. It needs --enable-preview.

static String classify(long value) {
    return switch (value) {
        case int i when i < 0 -> "negative int-range value";
        case int i            -> "int-range value: " + i;   // fits in an int exactly
        default               -> "needs a long";
    };
}

The int pattern matches only when the long converts to int without loss. That replaces manual range checks.

Concurrency and Threading Changes

Virtual Threads Without synchronized Pinning (Java 24)

In Java 21, a virtual thread blocking inside a synchronized block pinned its carrier thread. Under load, that could starve the scheduler. JEP 491 in Java 24 removes this limitation for most cases.

import java.util.concurrent.Executors;

public class VirtualDemo {
    private static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                executor.submit(() -> {
                    synchronized (LOCK) {          // no longer pins the carrier thread
                        blockingCall();
                    }
                });
            }
        } // close() waits for all tasks to finish
    }

    static void blockingCall() {
        try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

The program completes 10,000 tasks without exhausting platform threads. Legacy libraries built on synchronized become far friendlier to virtual threads, with no rewrite to ReentrantLock needed.

ScopedValue (Final in Java 25)

ScopedValue (JEP 506) shares immutable data with a call tree and its child virtual threads. It is cheaper than ThreadLocal because it has no mutable per-thread map to leak.

public class RequestHandler {
    private static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(USER, "alice").run(RequestHandler::handle);
        // USER is unbound again here
    }

    static void handle() {
        System.out.println("Processing as " + USER.get());  // Processing as alice
    }
}

The binding exists only inside run. Calling USER.get() outside the scope throws NoSuchElementException.

Structured Concurrency (Fifth Preview in Java 25)

StructuredTaskScope treats forked subtasks as one unit of work. Java 25 reworked the API around StructuredTaskScope.open() and Joiner policies. It stays preview, so compile with --enable-preview --release 25.

Response load(String id) throws InterruptedException {
    try (var scope = StructuredTaskScope.open()) {
        var user  = scope.fork(() -> findUser(id));
        var order = scope.fork(() -> findOrder(id));
        scope.join();                                // waits; propagates failure
        return new Response(user.get(), order.get());
    }
}

If either subtask fails, the default policy cancels the sibling and join() throws. No orphaned threads are left behind.

Library and API Additions

Stream Gatherers (Final in Java 24)

Gatherers add custom intermediate operations to streams. The built-ins cover windowing, folding, and scanning.

import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;

public class GatherDemo {
    public static void main(String[] args) {
        List<List<Integer>> fixed = Stream.of(1, 2, 3, 4, 5)
                .gather(Gatherers.windowFixed(2))
                .toList();
        System.out.println(fixed);   // [[1, 2], [3, 4], [5]]

        List<List<Integer>> sliding = Stream.of(1, 2, 3, 4, 5)
                .gather(Gatherers.windowSliding(3))
                .toList();
        System.out.println(sliding); // [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
    }
}

windowFixed produces non-overlapping batches, and windowSliding produces overlapping ones. Both replace hand-written index loops.

Foreign Function & Memory API (Final in Java 22)

The FFM API replaces JNI for most native interop. It offers safe off-heap memory and downcalls without C glue code.

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class NativeStrlen {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        MethodHandle strlen = linker.downcallHandle(
            linker.defaultLookup().find("strlen").orElseThrow(),
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment text = arena.allocateFrom("Java 25");
            long length = (long) strlen.invokeExact(text);
            System.out.println(length);  // 7
        }   // memory freed deterministically when the arena closes
    }
}

The confined Arena frees native memory at scope exit, which removes a whole class of leaks.

Other Final APIs Worth Tracking

API Version Purpose
Class-File API 24 Parse and generate bytecode without ASM
Key Derivation Function API 25 Standard KDF access (HKDF, Argon2 providers)
ML-KEM / ML-DSA 24 Post-quantum key encapsulation and signatures
Markdown doc comments 23 Write Javadoc with /// and Markdown
Stable Values 25 (preview) Lazy, constant-folded immutable fields

Runtime and Performance Improvements

Compact Object Headers (Product in Java 25)

JEP 519 shrinks the object header on 64-bit HotSpot from 12 bytes to 8 bytes. Programs with millions of small objects benefit most. It is opt-in:

java -XX:+UseCompactObjectHeaders -jar app.jar

Measure heap size and GC frequency before and after on your own workload, because gains depend on object shape.

AOT Cache (Java 24 and 25)

Ahead-of-time class loading and linking (JEP 483), plus method profiling (JEP 515), cut startup time. Java 25’s ergonomics change (JEP 514) reduces the workflow to two commands.

# 1. Training run: record and create the cache in one step
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App

# 2. Production run: reuse the cache
java -XX:AOTCache=app.aot -cp app.jar com.example.App

The cache is tied to the exact JDK build and classpath, so regenerate it after each upgrade or deployment.

Garbage Collector Changes

Collector Java 21 Java 25 Notes
G1 Default Default Improved barrier handling since 24
ZGC Generational optional Generational only Non-generational mode removed in 24
Shenandoah Single generation Generational (product) Enable with -XX:ShenandoahGCMode=generational
Parallel / Serial Available Available Unchanged strategy

Profiling With JFR

Java 25 adds CPU-time profiling (experimental, Linux), cooperative sampling, and method timing and tracing. These make JFR a stronger alternative to external sampling profilers.

Removed and Restricted Features: Migration Risks

Audit these items before moving from 21 to 25.

Change Version Action
Security Manager permanently disabled 24 Remove System.setSecurityManager() usage
32-bit x86 ports removed 24 (Windows), 25 (Linux) Move to 64-bit builds
Non-generational ZGC removed 24 Drop -XX:-ZGenerational
sun.misc.Unsafe memory-access methods warn at runtime 24 Migrate to VarHandle or FFM API
JNI use triggers warnings 24 Pass --enable-native-access explicitly
String Templates removed 23 Use String.format or formatted()

Best Practices: Bad Code vs. Good Code

Passing Context

Bad: mutable ThreadLocal with manual cleanup

private static final ThreadLocal<String> USER = new ThreadLocal<>();

void handle(String user) {
    USER.set(user);
    try { process(); }
    finally { USER.remove(); }   // forget this and you leak across pooled threads
}

Good: bounded, immutable ScopedValue

private static final ScopedValue<String> USER = ScopedValue.newInstance();

void handle(String user) {
    ScopedValue.where(USER, user).run(this::process);  // auto-unbound on exit
}

Constructor Validation

Bad: static helper hack to validate before super

public PositiveAmount(long cents) {
    super(check(cents));
}
private static long check(long c) {
    if (c <= 0) throw new IllegalArgumentException();
    return c;
}

Good: inline validation before super

public PositiveAmount(long cents) {
    if (cents <= 0) throw new IllegalArgumentException("Amount must be positive");
    super(cents);
}

Batching Stream Elements

Bad: index-based manual batching

List<List<Integer>> batches = new ArrayList<>();
for (int i = 0; i < data.size(); i += 100) {
    batches.add(data.subList(i, Math.min(i + 100, data.size())));
}

Good: declarative gatherer

List<List<Integer>> batches = data.stream()
        .gather(Gatherers.windowFixed(100))
        .toList();

Upgrade Checklist From Java 21 to Java 25

  1. Run your test suite on JDK 25 with no code changes and read every deprecation and --illegal-native-access warning.
  2. Remove Security Manager code and non-generational ZGC flags.
  3. Replace sun.misc.Unsafe memory access in your code and in dependencies.
  4. Update frameworks (Spring, Quarkus, Micronaut, Hibernate, and similar) to versions that officially support 25.
  5. Benchmark -XX:+UseCompactObjectHeaders and the AOT cache on a staging environment.
  6. Adopt ScopedValue, _, and flexible constructors in new code first. Refactor old code opportunistically.
  7. Keep preview features (structured concurrency, primitive patterns, stable values) out of production artifacts unless your team accepts --enable-preview constraints.

FAQ

Is Java 25 an LTS release?

Yes. Java 25 is a long-term support release, following Java 21. Oracle and other vendors provide extended updates, and most vendors support it for several years. Check your vendor’s support policy for exact dates.

Should I upgrade from Java 21 to Java 25?

Upgrade when your dependencies support it. You gain virtual threads without synchronized pinning, final ScopedValue, final Stream Gatherers, and lower startup and memory costs. Test first, because the Security Manager and 32-bit x86 removals can break older deployments.

What are the biggest new features between Java 21 and Java 25?

The most impactful are virtual threads without pinning (24), ScopedValue (25), flexible constructor bodies (25), Stream Gatherers (24), the FFM API (22), compact object headers (25), and the AOT cache (24 and 25).

Are structured concurrency and primitive patterns production-ready in Java 25?

No. Both remain in preview in Java 25 and require --enable-preview. Their APIs may still change, so avoid them in libraries you publish.

Hot this week

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Topics

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Build a Low-Cost AI Robot Arm With SO-101 and LeRobot

Build an SO-101 robot arm under $250, calibrate it, record demos, and train an ACT policy with LeRobot. Follow the full guide and start building.

Robot Foundation Models: GR00T, pi, Gemini Robotics, and Open Alternatives Compared

Compare robot foundation models: NVIDIA GR00T, Physical Intelligence π, Gemini Robotics 2, and open VLAs. Get latency math, code, and a pick guide.

How Much Does a Humanoid Robot Cost? Prices, Subscriptions, and Hidden Costs

Humanoid robot cost in 2026: prices from $4,900, $499/mo subscriptions, and hidden fees. See the full TCO breakdown and compare models now.

Related Articles

Popular Categories