Java 25: what’s new in the successor to Java 21?
Java 25 is the brand-new Long-Term Support (LTS) release of Java. It was released on September 16, 2025. It succeeds Java 21, which came out two years earlier and introduced numerous improvements, particularly in simplifying the language (Pattern Matching for Switch, Record Patterns) and in concurrency management with Virtual Threads.
So, what about Java 25? What new features and improvements does this release bring?
Let’s find out in this overview of the changes introduced since Java 21.
Java, a Language Accelerating Its Transformation
Java 25 brings a host of new features and improvements, continuing the momentum of previous releases. Since Java 21, several intermediate versions (Java 22, 23, and 24) have introduced valuable capabilities, particularly in simplifying the language and optimizing performance, such as reducing Java application startup times. They have also evolved APIs and strengthened security in anticipation of the future arrival of quantum computing, which could break today’s cryptographic algorithms.
In total, more than 20 JEPs (Java Enhancement Proposals) have been integrated and finalized since Java 21.
These advancements confirm that Java continues to strengthen its position as a modern, high-performance programming language, while staying true to its core principles of simplicity, portability, and robustness.
Java Language Enhancements
Java 25 introduces significant improvements to the language, aimed at boosting code readability and maintainability while simplifying its syntax. Here are the key new features added since Java 21:
JEP 456: Unnamed Variables & Patterns
Whether for coding style reasons or simply because Java requires it, developers often find themselves declaring variables in certain contexts without ever using them.
For example, the catch block is typically written in this form, where the exception parameter ex is not actually used:
String s = ...;
try {
int i = Integer.parseInt(s);
... i ...
}
catch (NumberFormatException ex) {
System.out.println("Bad number: " + s); // ex is not used
}
In Java 25, unused variables can be declared with the variable name _ (underscore).
For example, the previous catch block can be rewritten as follows:
String s = ...;
try {
int i = Integer.parseInt(s);
... i ...
}
catch (NumberFormatException _) { // Unnamed variable
System.out.println("Bad number: " + s);
}This syntax is also valid within method parameters, lambda expressions, pattern matching, and more.
JEP 467: Markdown Documentation Comments
Until now, Java documentation comments were primarily written in HTML, which often made writing and reading documentation more complex. With the introduction of JEP 467, Java 25 now allows the use of Markdown in documentation comments. This greatly simplifies the creation of documentation, making the process more intuitive and accessible for developers. Markdown support also enables better integration with various documentation tools and development platforms.
JEP 511: Module Import Declarations
Java 25 introduces the ability to import all required packages from a module into Java source code with a single import module declaration in the source file. This simplifies dependency management and improves code readability by reducing the number of lines needed to import multiple packages from the same module.
Example before Java 25:
import java.util.Map; // or import java.util.*;
import java.util.function.Function; // or import java.util.function.*;
import java.util.stream.Collectors; // or import java.util.stream.*;
import java.util.stream.Stream; // (can be removed)
...
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
Function.identity()));Example with Java 25:
import module java.base; // Import all exported packages from java.base
...
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
Function.identity()));
JEP 512: Compact Source Files and Instance Main Methods
Compared to other languages, Java is quite verbose. To simplify its syntax, it is now possible to write Java in a more compact form. This evolution is particularly useful for developers who are new to Java, as it allows them to get familiar with the language more easily without having to dive right away into more complex notions such as classes, packages, modules, or visibility modifiers, which can be confusing at an early stage of learning. It is especially suited for writing Java programs in a concise, script-like style.
This feature makes it possible to:
- Adopt a compact form of source files without declaring a superfluous class
- Replace the traditional
public static void main(String[] args)method with a simpler, more understandable instancemain()method - Include a new class in the
java.langpackage that provides simplified and more beginner-friendly I/O methods (instead of the classicSystem.out.println) - Automatically import standard APIs from the
java.langpackage when the code is written in the compact format (see point 1)
Example before Java 25:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}Example with Java 25:
main() {
IO.println("Hello, World! in compact style");
}
JEP 513: Flexible Constructor Bodies
In previous LTS releases, it was not possible to write statements before calling the parent class constructor (super()). In some cases, this could lead to unnecessary calls, especially when parameter validation was required before initializing the object.
With Java 25, the language now allows writing statements before the super() call, providing greater flexibility in managing object initialization.
Example code demonstrating this enhancement:
public class PositiveBigInteger extends BigInteger {
private final long max;
public PositiveBigInteger(long value, long max) {
// Code before calling super()
if (value <= 0)
throw new IllegalArgumentException("non-positive value");
this.max = max;
// Call super() last : Avoids unnecessary calls to the parent constructor if an exception is raised upstream (not possible before)
super(value);
}
}
Performance Improvements with Java 25
Java 25 marks a major step forward in performance improvements, particularly by integrating advancements from Project Leyden.
The goal of Project Leyden is to reduce the startup time and memory footprint of Java applications, thereby boosting their performance—especially in cloud-native and serverless environments. Until now, the main solution for achieving near-instant startup was to use native applications compiled with GraalVM Native Image, which can start within just a few milliseconds. However, this approach comes with several limitations: long and costly compilation times, partial support for reflection and certain libraries, and sometimes lower throughput compared to the traditional JVM.
With Java 25, OpenJDK aims to combine the strengths of the JVM (compatibility, dynamic optimizations, mature ecosystem) with significantly faster startup times, thanks to the introduction of an Ahead-of-Time caching mechanism that precompiles parts of the code.
Ahead-Of-Time (AOT) Cache (JEPs 483, 514, and 515)
Java 25 significantly improves application performance with the introduction of an Ahead-Of-Time (AOT) cache.
The AOT cache, introduced through JEPs 483 and 514, dramatically reduces Java application startup time. It is built during a training run of the application, which records the classes and methods used, then generates an optimized AOT cache for those classes and methods. At application startup, this cache is loaded into memory, reducing Just-In-Time (JIT) compilation overhead and improving overall startup performance.
JEP 515 further enhances the AOT cache by integrating profiling data collected by the JIT. This makes the cache more effective, as it is optimized based on the application’s actual runtime behavior.
Performance gains are particularly noticeable in applications with a large number of classes and methods, where startup times can be significantly reduced. Depending on the application, startup can be up to 50% faster.
To build the AOT cache, you need to add the following option:-XX:AOTCacheOutput:
$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...To run the application with the previously built AOT cache, you need to add the option:-XX:AOTCache:
$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
JEP 519: Compact Object Headers
Improvements have been made to object header management to reduce their size. By optimizing the structure of object headers, Java 25 lowers the memory footprint of applications—a benefit that is especially valuable for applications managing large numbers of objects in memory. This optimization also contributes to overall performance improvements by reducing pressure on the Garbage Collector (GC).
To validate the stability of this feature, Oracle and Amazon conducted extensive testing across hundreds of production services. The results showed memory usage reductions of up to 20% in some cases, processing time reductions of up to 10%, and GC cycles up to 15% less frequent.
This option is disabled by default. To enable it, you need to add the following JVM option:-XX:+CompactObjectHeaders
Garbage Collector (GC) Evolution
Java provides several Garbage Collectors (GCs) to meet the different needs of applications. In Java 25, improvements have been made to the G1, ZGC, and Shenandoah collectors.
Shenandoah is a low-latency GC designed to minimize application pauses by performing most of its collection work concurrently with application execution. Shenandoah is not the default GC in Java; it is better suited for applications with very large heaps, such as Big Data workloads. Through JEP 521, a generational mode has been added. The default “non-generational” mode remains available. To enable Shenandoah’s generational mode, add the following option to the JVM:-XX:ShenandoahGCMode=generational
Generational mode has also been added to the ZGC Garbage Collector via JEP 474, deprecating the non-generational mode. Since generational mode generally provides better performance than non-generational mode, future development will focus on this approach.
Finally, Java 25’s default GC, G1, has also received improvements through JEP 423. These enhancements aim to reduce G1 pause times during operations that involve JNI (Java Native Interface) functions.
Java 25: API Enhancements
The Java standard libraries continue to evolve with Java 25, with the goal of providing developers with even more functionality.
JEP 502: Scoped Values
Scoped Values are containers that allow immutable data to be efficiently shared between a method and its callees in the same thread, as well as with child threads, without relying on method parameters. Unlike variables, a ScopedValue is defined once and remains available for reading during a well-defined execution scope.
Scoped Values are especially useful in the context of Virtual Threads, as they provide lower memory and performance overhead compared to traditional ThreadLocals. Virtual Threads are lightweight and short-lived, and applications can run millions of them. Using ThreadLocals in this context can lead to excessive memory usage and even memory leaks if not properly cleaned up. Scoped Values are designed with Virtual Threads in mind, but they also work with traditional threads.
The following example shows an API that defines the connected user as a scoped value:
public class ScopedValuesExample {
// Logger
private static final Logger log = Logger.getLogger(ScopedValuesExample.class.getName());
// Declaring a ScopedValue
private static final ScopedValue<String> USERNAME = ScopedValue.newInstance();
public static void main(String[] args) {
// Define the scope with a value
ScopedValue.where(USERNAME, "Alice").run(() -> {
log.info("In the principal scope");
methodA(); // Can access USERNAME without parameters
}); // USERNAME is no longer accessible here
// Attempt to access outside scope (raises an exception)
try {
String user = USERNAME.get(); // Exception !
} catch (NoSuchElementException e) {
log.info("USERNAME is not accessible outside the scope");
}
}
private static void methodA() {
// Direct access to the value from any method within the scope
String user = USERNAME.get();
log.info("Method A - User: " + user);
methodB(); // Calling a deeper method
}
private static void methodB() {
// The value remains accessible even in nested calls
String user = USERNAME.get();
log.info("Method B - User: " + user);
}
}
JEP 485: Stream Gatherers
Stream Gatherers represent an enhancement to the Stream API. Gatherers make it possible to create custom intermediate operations for Streams. It’s worth noting that creating custom terminal operations was already possible through the Collector interface.
The following example reproduces the intermediate operation Gatherers.limit():
Gatherer<String, ?, String> limit() {
class Counter {
int counter;
Counter(int counter) { this.counter = counter; }
}
return Gatherer.ofSequential(
() -> new Counter(0),
(counter, element, downstream) -> {
if (counter.counter++ == 3) {
return false;
}
return downstream.push(element);
}
);
}However, it is less efficient to create custom intermediate operations that reproduce functionality already available (such as the limit() example above). These custom operations do not benefit from all the optimizations of the JIT compiler.
JEP 491: Synchronize Virtual Threads without Pinning
Virtual Threads were the major innovation introduced in Java 21, and their integration into the Java ecosystem has been further optimized. This feature targets two main goals:
- Ease of adoption – Making it simpler for existing Java libraries to work with Virtual Threads, without requiring changes to avoid the use of
synchronizedmethods or blocks. - Better diagnostics – Enhancing diagnostic tools to more effectively identify cases where Virtual Threads remain tied to platform threads (a phenomenon known as pinning), which prevents them from being released.
Virtual Threads remain the flagship evolution of Java 21, and their integration into the ecosystem has been further improved in Java 25.
JEP 484: Class-File API
With this enhancement, it is now possible to generate Java classes dynamically at runtime. This opens the door to advanced use cases such as on-the-fly code generation, dynamic proxy creation, and other scenarios where flexibility and dynamism are essential.
The following is a simple example of generating a Java class using the Class-File API:
ClassBuilder classBuilder = ...;
classBuilder.withMethod(
"fooBar",
MethodTypeDesc.of(CD_void, CD_boolean, CD_int),
flags,
methodBuilder -> methodBuilder.withCode(
codeBuilder -> {
Label label1 = codeBuilder.newLabel();
Label label2 = codeBuilder.newLabel();
codeBuilder.iload(1)
.ifeq(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
.goto_(label2)
.labelBinding(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
.labelBinding(label2);
.return_();
}
)
);The generated code is equivalent to the following Java code:
void fooBar(boolean z, int x) {
if (z)
foo(x);
else
bar(x);
}
JEP 454: Foreign Function & Memory API
The creation of the Foreign Function & Memory (FFM) API now provides access to code and data managed outside the JVM. This API enables secure invocation of native code (C, C++, etc.) and access to memory blocks outside the JVM heap.
The following code shows a simple example of using the FFM API to call the C function strlen, which computes the length of a string:
public class FFMTest {
public static void main(String[] args) throws Throwable {
// 1. Get a lookup object for commonly used libraries
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
// 2. Get a handle to the "strlen" function in the C standard library
MethodHandle strlen =
Linker.nativeLinker()
.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
// 3. Get a confined memory area (one that we can close explicitly)
try (Arena offHeap = Arena.ofConfined()) {
// 4. Convert the Java String to a C string and store it in off-heap memory
MemorySegment str = offHeap.allocateFrom("Happy Coding!");
// 5. Invoke the foreign function
long len = (long) strlen.invoke(str);
System.out.println("len = " + len);
}
// 6. Off-heap memory is deallocated at end of try-with-resources
}
}
JEP 472: Prepare to Restrict the Use of JNI
Java 25 still allows calling native code through either the Java Native Interface (JNI) or the Foreign Function & Memory (FFM) API. Since native code loading is restricted in the FFM API, a runtime warning is issued by default.
To ensure consistent behavior between JNI and FFM, the use of JNI also triggers a warning when executing native code.
Messages displayed when using JNI or the FFM API:
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by com.example.Main in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabledTo disable warning messages for all Java code, you can use the option:--enable-native-access=ALL-UNNAMED
java --enable-native-access=ALL-UNNAMED ...Deprecation of sun.misc.Unsafe Methods
JEPs 471 and 498 aim to prepare the ecosystem for the removal of memory access methods provided by the sun.misc.Unsafe API (introduced in 2002 and scheduled for complete removal starting with Java 26). The methods in this API are considered dangerous because they allow direct access to memory, which can lead to security and application stability issues.
It is recommended to replace calls to this API with the new Foreign Function & Memory (FFM) API introduced in Java 25 (see JEP 454 above).
A warning is issued whenever an application uses these methods, either directly or indirectly.
JEP 458: Launch Multi-File Source Code Programs
Since Java 11, it has been possible to run a program directly from a .java source file without a prior compilation step. The Java launcher automatically compiles the program in memory before execution.
JEP 458 takes this further by enabling simplified execution of Java programs that rely on additional source files. It allows launching a source file that uses a class defined in another source file, with the latter automatically compiled in memory as well. Source files are searched following the usual Java directory hierarchy that reflects the package structure.
Only the source files actually used by the main program are compiled in memory.
JEP 486: Permanently Disable the Security Manager
The Security Manager has rarely been used to secure server-side code, and maintaining it has proven costly. This feature was already deprecated in Java 17.
Due to its low adoption and the fact that most frameworks and tools have dropped support for it, the Security Manager has now been permanently disabled and is no longer available in Java 25.
JEP 503: Remove the 32-bit x86 Port
Support for the 32-bit x86 architecture has been removed in Java 25.
The maintenance cost of this architecture has become too high compared to its actual usage. Since the vast majority of modern systems now run on 64-bit architectures, continued support for 32-bit has become increasingly irrelevant.
Java Prepares for the Post-Quantum Era
Anticipating future developments in quantum computing—which could render today’s cryptographic algorithms vulnerable—it is essential for Java to start building the foundations for integrating hybrid, quantum-resistant encryption mechanisms in the years ahead.
With JEP 510, Java now supports the HKDF (HMAC-based Key Derivation Function) algorithm defined in RFC 5869. This algorithm is used to derive secret keys from an initial key and an optional salt, using an underlying cryptographic hash function.
Example of creating a derived secret key with HKDF-SHA256:
// Create a KDF object for the specified algorithm
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// Create an ExtractExpand parameter specification
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt).thenExpand(info, 32);
// Derive a 32-byte AES key
SecretKey key = hkdf.deriveKey("AES", params);
// Additional deriveKey calls can be made with the same KDF objectIn the future, further enhancements are expected, as Java plans to add additional KDF algorithms in upcoming JDK releases, such as Argon2.
In addition, JEPs 496 and 497 introduce encryption and digital signature mechanisms that are resistant to quantum attacks, based on post-quantum cryptography algorithms such as ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) and ML-DSA (Module-Lattice-Based Digital Signature Algorithm).
Java 25: Profiling at the Core of the JVM
Java Flight Recorder (JFR) has established itself as the reference tool for profiling Java applications.
Through JEP 518, its design has been rethought to reduce the impact on application performance while providing finer and more accurate performance analysis.
Through JEP 520, new capabilities have been added to collect tracing and method timing information. This makes it possible to gather reliable, precise data on method invocation counts, diagnose performance issues more effectively, and optimize code accordingly.
What’s Next for the Java Language?
Java 25 also includes features in incubation, preview, or experimental stages. These features are not yet stable and/or require community feedback before being permanently integrated into the JDK. They provide a glimpse of the future direction of the Java language and platform.
Here are the features present but not yet finalized in Java 25:
- JEP 507: Primitive Types in Patterns,
instanceof, andswitch(3rd Preview) – This aims to provide a uniform syntax for all pattern types (record, switch,instanceof), adding support for primitive types (currently not possible). - JEP 502: Stable Values (Preview) – This allows deferring the initialization of immutable class attributes to improve performance by avoiding unnecessary computations during object initialization (e.g., a
static finallogger that might never be used), while ensuring the value remains immutable after its first assignment. - JEP 505: Structured Concurrency (5th Preview) – Structured concurrency, enabled by Virtual Threads, is a modern approach to splitting tasks into subtasks and running them in parallel across different threads, while simplifying lifecycle management and, in particular, error handling.
- JEP 470: PEM Encodings of Cryptographic Objects (Preview) – This adds the ability to read and write cryptographic objects (keys, certificates, etc.) in the PEM (Privacy-Enhanced Mail) format, a widely used standard for storing and exchanging cryptographic data that Java previously did not support natively.
- JEP 489: Vector API (10th Incubation) – The 10th incubation of the Vector API, which aims to provide efficient vector operations for parallel data processing, delivering significant performance gains for compute-intensive applications.
Conclusion
Java 25 is an LTS release that brings numerous improvements and innovations across various areas: performance, security, code and API enhancements, profiling, and more.
With this version, OpenJDK continues to evolve the core of the Java language while accelerating the integration of Project Leyden’s work to improve application performance, particularly in terms of startup time and memory consumption, two critical aspects for cloud-native applications.
Java 25 also enhances the developer experience with new features that simplify code and make the language more accessible to beginners and developers coming from other languages.
Application profiling is further strengthened through several enhancements to Java Flight Recorder (JFR), offering more detailed and accurate performance analysis while reducing its overhead on applications.
Finally, Java 25 prepares for the future by introducing features that anticipate the challenges of quantum computing, including the creation of a KDF (Key Derivation Function) API.
Learn more:
https://openjdk.org/projects/jdk/25/
https://openjdk.org/projects/jdk/24