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
synchronizedas 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
- Run your test suite on JDK 25 with no code changes and read every deprecation and
--illegal-native-accesswarning. - Remove Security Manager code and non-generational ZGC flags.
- Replace
sun.misc.Unsafememory access in your code and in dependencies. - Update frameworks (Spring, Quarkus, Micronaut, Hibernate, and similar) to versions that officially support 25.
- Benchmark
-XX:+UseCompactObjectHeadersand the AOT cache on a staging environment. - Adopt ScopedValue,
_, and flexible constructors in new code first. Refactor old code opportunistically. - Keep preview features (structured concurrency, primitive patterns, stable values) out of production artifacts unless your team accepts
--enable-previewconstraints.
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.




