Java 25 (JDK 25) is here — and it’s an LTS release. That means most vendors will support it for years, and many teams will plan upgrades from JDK 17 or 21 directly to 25. In this post, I’ll walk through the highlights that matter in real projects: language changes, concurrency improvements (Project Loom), runtime and GC work, observability via JFR, security/crypto, and AOT ergonomics.
This post is based on the official OpenJDK pages and JEPs for JDK 25.
Quick Summary
| Area | Highlights |
|---|---|
| LTS Release | 18 JEPs included — see the full list below |
| Language & Syntax | Primitive types in patterns/switch (3rd preview), module import declarations, compact source files & instance main methods, flexible constructor bodies |
| Concurrency (Loom) | Structured Concurrency (5th preview), Scoped Values (finalized) |
| Observability (JFR) | CPU-time profiling (experimental), method timing & tracing, cooperative sampling |
| Performance & Memory | Compact object headers, Generational Shenandoah GC |
| Compute / SIMD | Vector API reaches its 10th incubator |
| Security & Crypto | PEM encodings (preview), Key Derivation Function API |
Language and Syntax Improvements
-
Primitive Types in Patterns, instanceof, and switch (Third Preview) — JEP 507
- Brings pattern matching to primitives, improving expressiveness in
switchandinstanceofscenarios. - Still a preview: enable with
--enable-previewwhile evaluating in production-like tests.
Object x = 42; // instanceof with a primitive pattern if (x instanceof int i) { System.out.println(i + 1); } // switch with primitive patterns switch (x) { case int i -> System.out.println("int: " + i); case long l -> System.out.println("long: " + l); case double d -> System.out.println("double: " + d); default -> System.out.println("other: " + x); } - Brings pattern matching to primitives, improving expressiveness in
-
Module Import Declarations — JEP 511
- Streamlines how code refers to modules and their content, complementing package-level imports.
// Import all exported packages from the java.sql module import module java.sql; // Then regular package imports work as usual import java.sql.Connection; import java.sql.DriverManager; class Demo { void main() { try (Connection conn = DriverManager.getConnection( "jdbc:postgresql://localhost/db", "u", "p")) { System.out.println(conn.getMetaData().getURL()); } catch (Exception e) { e.printStackTrace(); } } } -
Compact Source Files and Instance Main Methods — JEP 512
- Makes small programs and examples easier to write and run with a leaner source file format and support for instance
mainmethods.
// Instance main method (no public/static/String[] args) class HelloWorld { void main() { System.out.println("Hello, World!"); } }java HelloWorld.java - Makes small programs and examples easier to write and run with a leaner source file format and support for instance
-
Flexible Constructor Bodies — JEP 513
- Eases constraints in constructors, improving readability and reducing boilerplate in common patterns.
class A { A(int x) { System.out.println("A(" + x + ")"); } } class B extends A { private final int y; B(int y) { // Prologue: statements before the explicit constructor invocation if (y < 0) throw new IllegalArgumentException("y must be >= 0"); System.out.println("Preparing..."); // Explicit superclass constructor call super(y); // Epilogue: statements after the invocation this.y = y; System.out.println("Done."); return; // allowed in the epilogue (no expression) } }
Concurrency & Loom
-
Structured Concurrency (Fifth Preview) — JEP 505
- A higher-level API for managing related tasks as a single unit, leading to clearer code and predictable cancellation/error handling.
- Preview status lets the API evolve with real-world feedback.
import java.util.concurrent.StructuredTaskScope; String render() throws Exception { try (var scope = StructuredTaskScope.<String, Void>open()) { var userTask = scope.fork(() -> fetchUser()); var ordersTask = scope.fork(() -> fetchOrders()); scope.join(); // wait for both subtasks to complete return "%s | %s".formatted(userTask.get(), ordersTask.get()); } } -
Scoped Values — JEP 506
- Finalized. Provides a safe, immutable, thread-local-like mechanism for sharing data within a call scope, especially effective with virtual threads.
// Define a scoped value static final java.lang.ScopedValue<String> REQUEST_ID = java.lang.ScopedValue.newInstance(); void handle(Request req) { java.lang.ScopedValue.where(REQUEST_ID, req.id()).run(() -> { service(); }); } void service() { System.out.println("rid=" + REQUEST_ID.get()); }
Observability with JFR
| JEP | Feature | Description |
|---|---|---|
| JEP 509 | JFR CPU-Time Profiling (Experimental) | Better attribution of CPU time to Java code paths |
| JEP 520 | JFR Method Timing & Tracing | Lower-overhead method-level timing and trace events for latency/hotspot diagnosis |
| JEP 518 | JFR Cooperative Sampling | Improved sampling behavior and consistency across runtimes |
# Start a JFR recording and dump on exit
java -XX:StartFlightRecording=filename=profile.jfr,dumponexit=true -jar app.jar
# View CPU-time hot methods (new view in JDK 25)
jfr view cpu-time-hot-methods profile.jfr
# Record method timing for static initializers and dump to file
java '-XX:StartFlightRecording:method-timing=::<clinit>,filename=clinit.jfr' -jar app.jar
# View method timing results
jfr view method-timing clinit.jfr
Performance, Memory, and GC
| JEP | Feature | Impact |
|---|---|---|
| JEP 519 | Compact Object Headers | Reduces object header size to lower memory footprint and improve cache locality |
| JEP 521 | Generational Shenandoah | Generational mode for Shenandoah GC — better throughput and latency for long-lived services |
| JEP 503 | Remove 32-bit x86 Port | Cleans up legacy maintenance burden; modern Java focuses on x64 and ARM64 |
# Enable Shenandoah in generational mode
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar app.jar
AOT Ergonomics and Profiling
| JEP | Feature | Description |
|---|---|---|
| JEP 514 | AOT Command-Line Ergonomics | Smoother CLI options and defaults when experimenting with AOT builds |
| JEP 515 | AOT Method Profiling | Targeted profiling data to guide AOT compilation decisions |
# One-step training + cache creation
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App
# Production run using the AOT cache
java -XX:AOTCache=app.aot -cp app.jar com.example.App
Vector API (Incubator)
-
Vector API (Tenth Incubator) — JEP 508
- Continued refinement of a portable, explicit SIMD API for data-parallel operations, mapping efficiently to modern CPU instructions.
- As an incubator, it remains behind
--add-modules jdk.incubator.vectorfor now.
import jdk.incubator.vector.*; static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED; void vectorCompute(float[] a, float[] b, float[] c) { int i = 0; int ub = SPECIES.loopBound(a.length); for (; i < ub; i += SPECIES.length()) { var va = FloatVector.fromArray(SPECIES, a, i); var vb = FloatVector.fromArray(SPECIES, b, i); var vc = va.mul(va).add(vb.mul(vb)).neg(); vc.intoArray(c, i); } for (; i < a.length; i++) { c[i] = -(a[i] * a[i] + b[i] * b[i]); } }# Run with the incubator module java --add-modules jdk.incubator.vector VectorDemo
Security & Cryptography
| JEP | Feature | Description |
|---|---|---|
| JEP 470 | PEM Encodings (Preview) | First-class support for reading/writing PEM-encoded material — simplifies interoperability with common tooling |
| JEP 510 | Key Derivation Function API | Standardized APIs for modern KDFs, making secure key derivation more approachable and consistent |
// KDF (HKDF-SHA256) example — derive a 32-byte AES key
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.spec.HKDFParameterSpec;
byte[] initialKeyMaterial = /* ... */ null;
byte[] salt = /* ... */ null;
byte[] info = /* ... */ null;
KDF hkdf = KDF.getInstance("HKDF-SHA256");
AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt)
.thenExpand(info, 32);
SecretKey key = hkdf.deriveKey("AES", params);
// PEM encode/decode (Preview)
import java.security.PEMEncoder;
import java.security.PEMDecoder;
import java.security.KeyPair;
import java.security.interfaces.ECPublicKey;
// Encode a key pair as PEM text
var pe = PEMEncoder.of();
String pem = pe.encodeToString(new KeyPair(publicKey, privateKey));
// Decode with the expected type
var pd = PEMDecoder.of();
ECPublicKey pub = pd.decode(pem, ECPublicKey.class);
References
- JDK 25 overview: https://openjdk.org/projects/jdk/25/
- JEP index: https://openjdk.org/jeps/0
Conclusion
JDK 25 is a solid LTS that continues the steady modernization of Java: safer and clearer concurrency, better profiling, smarter memory layout, and practical language polish. If you’re on JDK 17 or 21, this is a compelling target for your next upgrade window — especially if you benefit from Loom, JFR improvements, or reduced memory overhead.