Written by
Achraf Hasbi
,
Practice Leader,
coach
and Java Expert at MARGO.
Top Java 25 features you should know
Java 25: Performance, Security, and Productivity
The release of Java 25 features marks a significant milestone for the ecosystem. As part of Oracle’s regular release cadence, Java 25 introduces several features that enhance language expressiveness and optimise JVM performance.
This version includes 18 JEPs: 12 finalised, 4 in preview, 1 experimental, and 1 incubator.
In this article, we explore the new features of Java 25, focusing on the finalised JEPs and their impact on Java development.
Download OpenJDK 25 here: OpenJDK 25
JVM: New Evolutions in Java 25
JEP 503: Remove the 32-bit x86 Port
JEP 503 proposes the removal of the 32-bit x86 port from the JDK. Following its deprecation in JEP 501, this decision aims to eliminate the associated source code and build support.
The primary driver behind this removal is the increasing maintenance burden of the 32-bit x86 port. Adapting it to support modern features—such as Project Loom, the Foreign Function & Memory (FFM) API, and the Vector API—has proven costly. By permanently removing this port, OpenJDK developers can accelerate the delivery of new features and enhancements.
JEP 514: Early Command-Line Ergonomics
JEP 514 simplifies the creation of Ahead-Of-Time (AOT) caches, which speed up Java application startup times by streamlining the commands required for common use cases.
Before JEP 514, creating an AOT cache involved a two-step process:
- 1. Training: Running the application to record its behaviour.
- 2. Creation: Generating the AOT cache from the recorded data.
This process was cumbersome and often left behind temporary configuration files. JEP 514 addresses these issues by introducing a one-step process that is both efficient and user-friendly, paving the way for broader adoption of AOT optimisations as Project Leyden progresses.
JEP 514 introduces a new command-line option, -XX:AOTCacheOutput, in the java launcher. When used on its own, this option allows the launcher to execute both the training and cache creation steps in a single command. Additionally, a new environment variable, JDK_AOT_VM_OPTIONS, allows for passing specific command-line options for cache creation without affecting the training run.
Before JEP 514
# Step 1: Training run
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App
# Step 2: Create AOT cache
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot app.jar com.example.App
# Production run
java -XX:AOTCache=app.aot -cp app.jar com.example.App
After JEP 514
# Single command to train and create AOT cache
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App
# Production run
java -XX:AOTCache=app.aot -cp app.jar com.example.App
JEP 515: Early Method Profiling
JEP 515 enables the storage and reuse of method execution profiles from a previous run (training) during application startup. The goal is to allow the HotSpot JVM’s JIT compiler to generate native code immediately upon startup, rather than waiting for profiles to be collected during initial execution.
This feature extends the existing AOT cache (introduced in JEP 483) to store not only classes loaded or linked at startup but also method execution profiles collected during training. It does not disable or replace runtime profiling: even with cached profiles, the JVM continues to collect profiling data in production, as real-world behaviour may differ from the training environment.
JEP 518: Cooperative JFR Sampling
JEP 518 improves the stability of JDK Flight Recorder (JFR) when collecting asynchronous samples of Java thread stacks. Instead of analysing stack frames at arbitrary points—which can lead to crashes or incorrect stack traces—JFR now traverses and analyses stacks only at safepoints.
To mitigate the issues caused by restricting stack walks to safepoints (known as safepoint bias), JFR uses a cooperative sampling approach: it quickly records „sample requests” (for instance, just the program counter and stack pointer) and defers the full stack capture until the target thread reaches its next safepoint.
Under JEP 518, when it is time to take a JFR execution time sample:
- 1. The sampling thread suspends the target thread and records only lightweight information (program counter + stack pointer) in a request. It does not attempt to analyse or traverse the stack immediately (unless already at a safepoint).
- 2. It enqueues this „sample request” (in a thread-local queue) and then allows the target thread to resume execution until its next safepoint.
- 3. At the safepoint, the target thread checks for pending sample requests; for each one, it reconstructs a secure stack trace and emits the sampling event.
JEP 519: Compact Object Headers
JEP 519 promotes Compact Object Headers from experimental to product (stable) status in JDK 25. This means they no longer require the „experimental” flag to be enabled.
Before JEP 519
java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -jar myapp.jar
After JEP 519
java -XX:+UseCompactObjectHeaders -jar myapp.jar
Compact object headers were introduced in JDK 24 under JEP 450 as an experimental feature to reduce the size of object headers. Since then, they have demonstrated strong stability and performance: in production (across Oracle and Amazon services) and during benchmarks, they have delivered memory savings, reduced CPU time, and fewer GC cycles.
JEP 520: JFR Method Timing & Tracing
JEP 520 adds method timing and tracing capabilities to JDK Flight Recorder (JFR) through bytecode instrumentation. Two new JFR events are introduced: jdk.MethodTiming (for recording execution counts and times) and jdk.MethodTrace (for tracing individual method calls with stack traces). The methods to be timed or traced can be selected via filters, configuration, jcmd, JMX, or JVM startup arguments.
JEP 521: Generational Shenandoah
JEP 521 promotes the generational mode of the Shenandoah garbage collector to a product feature in JDK 25. Previously, this mode was experimental (introduced in JDK 24 via JEP 404).
In JDK 24, to use Shenandoah’s generational mode, you had to pass specific VM command-line options, including:
-XX:+UseShenandoahGC
-XX:+UnlockExperimentalVMOptions
-XX:ShenandoahGCMode=generational
In JDK 25, as this is now a product feature, -XX:+UnlockExperimentalVMOptions is no longer required. You can enable generational mode simply with:
-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=generational
JEP 511: Module Import Declarations
JEP 511 introduces Module Import Declarations to the Java language. A module import declaration takes the form import module M; and allows the import of all public top-level classes and interfaces from every package exported by module M, including those that module M depends on (transitively) where applicable. This feature aims to simplify the use of modular APIs by reducing the number of standard import statements. It is finalised in JDK 25, following preview phases in previous versions.
Before JEP 511
// Without module import
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.nio.file.Path;
import java.io.IOException;
public class MyApp {
public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
Path p = Path.of("file.txt");
// ... etc
}
}
You require separate import instructions for each specific package/on-demand package or class.
After JEP 511
// With module import
import module java.base;
import module java.util;
import module java.io;
public class MyApp {
public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
Path p = Path.of("file.txt");
// ... etc
}
}
Even more simply
import module java.base;
import module java.io;
public class MyApp {
public Map<String, String> process(List<String> input, Function<String, String> fn) throws IOException {
Path p = Path.of("file.txt");
// etc
}
}
Since java.base imports java.util and java.nio.file, importing java.base modules allows you to integrate many of these elements without having to import each package/class individually. (Note: depending on how modules export packages and transitive requirements, some module imports may already include the necessary classes, thereby reducing import clutter.)
JEP 512: Compact Source Files and Instance Main Methods
JEP 512 finalises a set of features designed to simplify writing and reading Java programmes—particularly short or „beginner” programmes—without introducing a new dialect. It allows for compact source files: for small programmes, there is no need to declare a class, module, or package; you can write methods and fields directly.
It also introduces a new java.lang.IO class for basic console I/O, allowing beginners to write IO.println(...) instead of System.out.println(...).
Before JEP 512
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
After JEP 512
void main() {
IO.println("Hello, World!");
}
JEP 513: Flexible Constructor Bodies
JEP 513 allows statements to be inserted before an explicit constructor invocation (i.e., super(...) or this(...)) within a constructor body. These statements must not reference the object under construction (using this), except to initialise its own yet-to-be-initialised attributes.
Before JEP 513
class Person {
int age;
Person(int age) {
if (age < 0)
throw new IllegalArgumentException("Age must be >= 0");
this.age = age;
}
}
class Employee extends Person {
String officeID;
Employee(int age, String officeID) {
// Must call super first (implicitly or explicitly)
super(age);
if (age < 18 || age > 67)
throw new IllegalArgumentException("Employee age must be between 18 and 67");
this.officeID = officeID;
}
}
After JEP 513
class Person {
int age;
Person(int age) {
if (age < 0)
throw new IllegalArgumentException("Age must be >= 0");
this.age = age;
}
}
class Employee extends Person {
String officeID;
Employee(int age, String officeID) {
// Prologue: early validation
if (age < 18 || age > 67)
throw new IllegalArgumentException("Employee age must be between 18 and 67");
// Prologue: initialize subclass field before super
this.officeID = officeID;
super(age); // Now allowed after some checks and field assignment
}
}
API
JEP 506: Scoped Values
JEP 506 introduces Scoped Values, which enable the sharing of immutable data within a thread, between a method and its callers, and with child threads. Scoped Values are easier to understand and more lightweight in terms of memory and execution time than ThreadLocal, especially when used with Virtual Threads (JEP 444) and Structured Concurrency (JEP 505).
Before JEP 506 (using ThreadLocal)
public class Framework {
private static final ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();
public void serve(Request request, Response response) {
FrameworkContext ctx = createContext(request);
CONTEXT.set(ctx);
try {
Application.handle(request, response);
} finally {
CONTEXT.remove(); // need to clean up to avoid leaks
}
}
public PersistedObject readKey(String key) {
FrameworkContext ctx = CONTEXT.get();
if (ctx == null) {
throw new IllegalStateException("No context");
}
Database db = getDatabase(ctx);
return db.readKey(key);
}
}
After JEP 506 (using Scoped Values)
import java.lang.ScopedValue;
import static java.lang.ScopedValue.where;
public class Framework {
private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();
public void serve(Request request, Response response) {
FrameworkContext ctx = createContext(request);
where(CONTEXT, ctx).run(() -> {
Application.handle(request, response);
});
}
public PersistedObject readKey(String key) {
FrameworkContext ctx = CONTEXT.get(); // safe to call here if inside a run
if (ctx == null) {
throw new IllegalStateException("No context");
}
Database db = getDatabase(ctx);
return db.readKey(key);
}
}
JEP 510: Key Derivation Function API
JEP 510 finalises the Key Derivation Function (KDF) API in JDK 25. Previously introduced in JEP 478 and delivered as a preview in JDK 24, it is now part of the standard JDK 25 APIs. This JEP introduces a new javax.crypto.KDF class for key derivation algorithms, such as HKDF (HMAC-Extract-and-Expand) and others. It provides methods for deriving SecretKey (deriveKey) and raw data (deriveData), parameterised by algorithm specifications. It also includes parameter specification classes for HKDF (e.g., extract, expand, extract-then-expand).
Key Derivation Functions are widely used in modern cryptography: to generate multiple keys from a shared secret, in protocols such as hybrid key exchange, TLS 1.3, HPKE, etc. Having a standard, well-designed Java API for KDFs is essential for accuracy, interoperability, and maintainability.
Before JEP 510
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.SecretKey;
import java.security.SecureRandom;
public class HKDFExampleBefore {
public static SecretKey deriveAesKey(byte[] ikm, byte[] salt, byte[] info) throws Exception {
// Extract step
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec saltKey = new SecretKeySpec(salt, "HmacSHA256");
hmac.init(saltKey);
byte[] prk = hmac.doFinal(ikm);
// Expand step
byte[] okm = new byte[32]; // 256 bits
byte[] t = new byte[0];
int iterations = (int) Math.ceil((double)okm.length / hmac.getMacLength());
byte[] output = new byte[0];
for (int i = 1; i <= iterations; i++) {
hmac.init(new SecretKeySpec(prk, "HmacSHA256"));
hmac.update(t);
hmac.update(info);
hmac.update((byte) i);
t = hmac.doFinal();
output = concat(output, t);
}
byte[] aesKeyBytes = Arrays.copyOf(output, 32);
return new SecretKeySpec(aesKeyBytes, "AES");
}
}
After JEP 510
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.HKDFParameterSpec;
public class HKDFExampleAfter {
public static SecretKey deriveAesKey(byte[] ikm, byte[] salt, byte[] info) throws Exception {
// Instantiate the KDF with algorithm name
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// Build parameter spec: do extract then expand
AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract()
.addIKM(ikm)
.addSalt(salt)
.thenExpand(info, 32); // derive 32 bytes
// Derive AES key directly
SecretKey aesKey = hkdf.deriveKey("AES", params);
return aesKey;
}
}
Preview / Experimental / Incubator JEPs in Java 25
While this article focuses on the finalised JEPs in Java 25—features you can safely use in production today—this release also includes several preview, incubator, and experimental JEPs. These represent features that are still evolving: previews are nearly complete but may undergo minor adjustments; incubators expose APIs for early feedback; and experimental features are exploratory and could be significantly modified or removed. Here is an overview of what’s coming next:
JEP 470: PEM Encodings of Cryptographic Objects (Preview)
Adds an API to encode and decode cryptographic objects (public/private keys, certificates, CRLs) in the widely-used PEM format.
JEP 502: Stable Values (Preview)
Stable values are objects containing immutable data, but unlike final fields, their initialisation can be deferred („at most once initialisation”) and they are subsequently treated by the JVM almost as constants.
JEP 505: Structured Concurrency (Fifth Preview)
Provides APIs (primarily StructuredTaskScope) to treat a group of tasks (threads, including virtual threads) as a single structured unit: starting them together, waiting, handling failures/cancellations cleanly, and managing lifetimes as blocks.
JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
Extends pattern matching, instanceof, and switch so that primitive types (e.g., int, long, float) can be used in pattern contexts, not just reference types.
JEP 508: Vector API (10th Incubator)
Provides an incubator API to express vector computations (SIMD-style), allowing Java programmes to explicitly write vector operations that map to optimal CPU vector instructions at runtime. This is not yet finalised; it is currently in its tenth incubation phase.
JEP 509: JFR CPU Time Profiling (Experimental)
Adds CPU time profiling to Java Flight Recorder (on Linux, experimental). Instead of sampling based on the wall-clock or elapsed time, you get samples based on actual CPU time consumed, including native code. New event type: jdk.CPUTimeSample.
Conclusion
Java 25 reaffirms the steady evolution of the platform, combining mature, production-ready features with future-facing innovation. The finalised JEPs bring tangible improvements in performance, security, and developer productivity, while the preview, incubator, and experimental features offer a glimpse into the future of the language and the JVM. Together, they demonstrate a clear commitment to keeping Java modern, expressive, and high-performing. As always, adopting stable features today while keeping an eye on upcoming ones will allow you to benefit from the best of both worlds: reliability and innovation.
What does Java 25 bring to the table compared to Java 21?
Java 25 introduces several finalised features such as simplified module imports (JEP 511), compact source files (JEP 512), and flexible constructors (JEP 513). It also includes significant JVM enhancements (JEP 518–521) and enriched APIs, alongside preview and experimental features designed to shape the future of the language.
Are the new features backward compatible with previous Java versions?
Yes, finalised JEPs are backward compatible and production-ready. Preview or experimental JEPs are optional and may require code adjustments or specific flags to work with legacy codebases.
Is it safe to use preview or incubator JEPs?
These are intended for experimentation. While you can test them to familiarise yourself with upcoming features, they are subject to change or removal in future releases and are not recommended for mission-critical production environments.
Why is the Module Import (JEP 511) feature useful?
It simplifies import statements by reducing the number of lines required to include entire modules. This makes the code significantly cleaner, more readable, and easier to maintain.
Should I upgrade my environment to Java 25 immediately?
For production projects, we recommend first verifying compatibility with your existing dependencies. New features can be adopted gradually, starting with the finalised JEPs to ensure stability.
What are the JVM performance gains in Java 25?
The JVM benefits from several optimisations, including Early Method Profiling (JEP 515), Cooperative JFR Sampling (JEP 518), and the now-stable Generational Shenandoah (JEP 521). Together, these improve application performance, memory efficiency, and overall stability.
Where can I find official resources for Java 25 and its JEPs?
Detailed documentation for all JEPs is available at openjdk.org, and Java 25 builds can be downloaded from jdk.java.net/25.