Java 25

Java 25 - The new features

,

In my previous contribution was about the features still under development that are included as a preview in Java 25. This is followed by the functionalities that have been finalized in Java 25 and are ready for productive use.

Java 25 was released on 16.09.2025 and, like every six-monthly release, has brought with it a number of new functionalities. And even if it there are basically no LTS releases for the Java language itself (although it is often phrased like this), most publishers will probably offer extended support for their Java 25 distributions.

JEP-506: Scoped Values

Scoped values make it possible to make data available within a call hierarchy without having to pass it on explicitly via method parameters. The scope of the shared data is clearly limited to a specific execution context, including all directly and indirectly called methods. Once the relevant section has been completed, the value is released again.

As with ThreadLocal the value is also linked to the currently running thread. A ThreadLocal variable can therefore take on different values in different threads. This means, for example, that it is possible to link user sessions in a web framework via a ScopedValue to be provided:

public class WebFramework {

    final static ScopedValue<UserSession> USER_SESSION = ScopedValue.newInstance();

    private final SessionStore sessionStore;
    private final Service service;

    public WebFramework(SessionStore sessionStore, Service service) {
        this.sessionStore = sessionStore;
        this.service = service;
    }

    void doRequest(Request request, Response response) {
        UserSession userSession = sessionStore.currentSession();

        ScopedValue.where(USER_SESSION, userSession)
                .run(() -> service.handle(request, response));
        // ...
    }
}

class Service {

    public void handle(Request request, Response response) {
        UserSession session = WebFramework.USER_SESSION.get();
        if (session.isLoggedIn()) {
            // ...
        }
    }
}

First of all, a ScopedValue-instance of the type UserSession is instantiated (line 3). The reference is static so that it can also be accessed from other parts of the code.

In the doRequest(…)-method, the userSession is retrieved from the store (line 14). It is then passed to the where(…)-method from ScopedValue (line 16), and then directly run(…) (line 17), where the processing is delegated to the service instance.

In the handle(…)-method of the Service class, the user session can now be called via get() can be read and used from the ScopedValue instance.

The in where(…) set user session (line 17) is only available within the scope of the run(…)-method is valid. If you were to try in line 18 via USER_SESSION.get() to access the previously set userSession, the result would be a NoSuchElementException.

ScopedValues are lighter than ThreadLocal. In particular, they are suitable for use together with virtual threads, of which there can be very many, in contrast to conventional threads. ScopedValues can also be used as ThreadLocals can be passed on to child threads, but are less resource-hungry. As ScopedValues are immutable, they also contribute to more robust and higher-performance concurrent code (-> to the JEP-506).

JEP-510: Key Derivation Function API

Included for the first time as a preview in Java 24, the Key Derivation Function API is published unchanged as an official Java feature just one release later.

Key Derivation Functions (KDFs) use cryptographic inputs such as base keys, salt and pseudo-random functions to generate new, strong keys. They allow the reproducible and secure generation of different keys, similar to password hashing. KDFs extract or extend key material by combining a keyed hash with additional entropy. Given the increasing threat of quantum computing, post-quantum secure cryptography is becoming more important. The Java platform aims to facilitate the transition to quantum-safe algorithms with Hybrid Public Key Encryption (HPKE) and a new KDF API. This promotes interoperability, supports standards such as PKCS#11 and enables more modern password hashing methods (e.g. Argon2). This will significantly increase the security and flexibility of future applications.

Code example from the JEP:

// 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 object

-> to the JEP-510

JEP-511: Module Import Declarations

With the new "import module" statement, it is possible to import all classes exported by a module with just one statement:

import module java.base;
import module java.sql;

// resolve ambiguous import of java.util.Date and java.sql.Date
import java.util.Date;

public class ModuleImports {

    public static void main(String[] args) throws Exception {
        Stream<String> myStream = Stream.of("Alfred", "Bea", "Charlotte");
        List result = myStream.filter(n -> n.length() > 3).toList();
        Function<Integer, Integer> square = (i) -> i * i;
        Files.list(Paths.get(".")).forEach(System.out::println);

        Connection con = DriverManager.getConnection("jdbc:postgresql://localhost/test");
        XMLGregorianCalendar cal = DatatypeFactory.newInstance()
                .newXMLGregorianCalendar("2024-09-11T18:17:42.123Z");
        
        Date d = new Date();
    }
}

Thanks to the module imports from java.base and java.sql are already all in the main-method, so that these do not have to be imported explicitly. By importing java.base you get access to java.util.stream.Stream, java.util.List, java.util.function.Function, java.nio.file.Files and java.nio.file.Paths.

The classes java.sql.Connection and java.sql.DriverManager are created by importing the module java.sql available.

As the module java.sql a transitive dependency on the module java.xml implicitly the module java.xml imported, so that the classes javax.xml.datatype.XMLGregorianCalendar and javax.xml.datatype.DatatypeFactory can be used without further imports.

As both the module java.base as well as java.sql a class called Date export, there is an ambiguity here (java.util.Date and java.sql.Date). To dissolve these java.util.Date is explicitly imported again so that it is used in line 19.

Incidentally, the importing class does not have to be in a module (-> to the JEP-511).

JEP-512: Compact Source Files and Instance Main Methods

To make learning Java as easy as possible for beginners, the simplest Java program now looks like this:

void main() {
    IO.println("Hello World");
}

This means that the first sense of achievement comes quickly and easily without having to deal with concepts such as classes, static methods or method visibility. Experienced developers can write small programs with less code without having to use constructs that are intended for large code bases.

The JVM proceeds as follows when selecting the method to be executed: If there is a main-method with parameter String[] argsit is executed. If such a method does not exist, the method main() executed without parameters, if available. Whether static or not is irrelevant.

void main(String[] args) {  // wird ausgeführt
    IO.println("Hello World!"); 
}

static void main() {
    IO.println("I won't be called!");
}

In addition, the class java.lang.IO which is available without import and is intended to simplify line-based input and output (-> to the JEP-512).

JEP-513: Flexible Constructor Bodies

Up to now, calls from super(…) resp. this(…) always come first in constructors. With this JEP, it is now possible to place certain statements in front of these calls. However, there are restrictions for such statements, for example they may not contain any references to the instance to be created. Calls to static methods are therefore permitted, for example.

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(String val) {
        String trimmed = val.trim();
        if (trimmed.startsWith("-")) {
            throw new IllegalArgumentException("Negative values are not allowed");
        }
        super(trimmed);
    }
}

Before the super(…)-call, it is now possible to perform a validation as in the class shown above. Previously, the validation would either have been carried out after the super-call and, in the event of an error, unnecessary work may be carried out. The alternative would be val in the super-call into a validation method. In contrast, the code shown above is both more efficient and easier to understand.

For this() calls, the same applies, as the following example shows:

public class EMailAddress {

    public EMailAddress(String eMailAddress) {
        if (!eMailAddress.contains("@")) {
            throw new IllegalArgumentException(
                  "Invalid e-mail address: " + eMailAddress);
        }
        String[] parts = eMailAddress.split("@");
        this(parts);
    }

    private EMailAddress(String[] parts) {
        // ...
    }
}

-> to the JEP-513

JEP-514: Ahead-of-Time Command-Line Ergonomics

Ahead-of-time caches, introduced by JEP-483 in Java 24, accelerate the start of Java applications. Until now, 2 steps were necessary to create an AOT cache (for details see here).

To make cache generation more convenient for the standard case, the new command line option AOTCacheOutput which can be used to specify the target file for the AOT cache. This means that the AOT cache can now be created with just one command, without the detour via a .aotconf-file:

$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App

This call first starts a training run (AOTMode=record), and then creates the AOT cache in the specified file app.aot.

This can then be used to run the program with an accelerated start time:

$ java -XX:AOTCache=app.aot -cp app.jar com.example.App

In addition, a new environment variable called JDK_AOT_VM_OPTIONS which can be used to specify command line options that only affect cache generation (AOTMode=record), without the training run (AOTMode=record) (for the JEP-514).

JEP-515: Ahead-of-Time Method Profiling

At the beginning of the runtime of a Java program, the JVM devotes some of its resources to the task of creating method profiles in order to identify so-called "hot methods" that are called frequently and require a lot of resources, such as CPU time. These are then compiled into native code by the just-in-time compiler. As a result, the program runs less efficiently during this warm-up phase.

The warm-up time can be reduced by moving the profile creation to training runs. The AOT cache, which holds class information for a faster program start, now also stores method profiles that would otherwise have to be collected during the start phase of the productive execution of the program.

These profiles created in training runs do not prevent additional profiling during the production run, as the behavior of an application in production may differ from the behavior observed in training. Even with previously created profiles, the HotSpot JVM continues to profile and optimize the application during execution, combining the benefits of AOT profiling, online profiling and JIT compilation. As a result, the JIT compiler is executed earlier and with greater accuracy, and the profiles are used to optimize the hot methods, shortening the warm-up phase. As JIT compilation is performed in parallel with program execution, the actual warm-up time can be shorter if sufficient hardware resources are available (for example JEP-515).

JEP-518: JFR Cooperative Sampling

The JDK Flight Recorder (JFR) can create a runtime profile that shows which program elements take up most of the time. For this purpose, the execution stacks of program threads are recorded at fixed intervals, for example every 20 milliseconds. Tools such as jfr and JDK Mission Control can aggregate a series of such recordings into a text-based or graphical profile.

This sampling mechanism is being redesigned to make the JFR more stable and efficient. For details on the functionality of the sampling and the optimizations, please refer to the JEP (-> to the JEP-518).

JEP-520: JFR Method Timing & Tracing

Timing and tracing of method calls can be very helpful for performance optimization or the search for the causes of bugs. While there are already good tools for the development phase, such as Java Microbenchmark Harness (JMH) and debugger, you still have to make do with workarounds in test and production.

To fill this gap, two new JFR events will be introduced: jdk.MethodTiming and jdk.MethodTrace. A filter can be used to specify the methods for which the events are to be recorded.

For example, you can find out as follows what causes the resize()-method of the class HashMap is triggered:

$ java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr …
$ jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr
jdk.MethodTrace {
    startTime = 00:39:26.379 (2025-03-05)
    duration = 0.00113 ms
    method = java.util.HashMap.resize()
    eventThread = "main" (javaThreadId = 3)
    stackTrace = [
      java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 636
      java.util.HashMap.put(Object, Object) line: 619
      sun.awt.AppContext.put(Object, Object) line: 598
      sun.awt.AppContext.<init>(ThreadGroup) line: 240
      sun.awt.SunToolkit.createNewAppContext(ThreadGroup) line: 282
      sun.awt.AppContext.initMainAppContext() line: 260
      sun.awt.AppContext.getAppContext() line: 295
      sun.awt.SunToolkit.getSystemEventQueueImplPP() line: 1024
      sun.awt.SunToolkit.getSystemEventQueueImpl() line: 1019
      java.awt.Toolkit.getEventQueue() line: 1375
      java.awt.EventQueue.invokeLater(Runnable) line: 1257
      javax.swing.SwingUtilities.invokeLater(Runnable) line: 1415
      java2d.J2Ddemo.main(String[]) line: 674
    ]
}

If an application takes a long time to start, a time measurement of all static initialization blocks could provide information about the cause:

$ java '-XX:StartFlightRecording:method-timing=::<clinit>,filename=clinit.jfr' ...
$ jfr view method-timing clinit.jfr

                                 Method Timing

Timed Method                                           Invocations Average Time
------------------------------------------------------ ----------- ------------
sun.font.HBShaper.<clinit>()                                     1 32.500000 ms
java.awt.GraphicsEnvironment$LocalGE.<clinit>()                  1 32.400000 ms
java2d.DemoFonts.<clinit>()                                      1 21.200000 ms
java.nio.file.TempFileHelper.<clinit>()                          1 17.100000 ms
sun.security.util.SecurityProviderConstants.<clinit>()           1  9.860000 ms
java.awt.Component.<clinit>()                                    1  9.120000 ms
sun.font.SunFontManager.<clinit>()                               1  8.350000 ms
sun.java2d.SurfaceData.<clinit>()                                1  8.300000 ms
java.security.Security.<clinit>()                                1  8.020000 ms
sun.security.util.KnownOIDs.<clinit>()                           1  7.550000 ms
...
$

It is also possible to filter at class level so that all methods of a class are timed or traced. In the same way, you can apply the filter to all methods that are provided with a specific annotation.

Several filters are also possible, and instead of specifying them in the command line, they can also be written to a configuration file (-> to the JEP-520).

JEP-519: Compact Object Headers

Compact object headers were developed as part of JEP-450 was introduced as an experimental feature in Java 24. Since then, the functionality has been extensively tested and is now a product feature in Java 25:

$ java -XX:+UseCompactObjectHeaders ...

Activation via this command line parameter reduces object headers from 96-128 to 64 bits, which in turn leads to a reduction in total memory consumption (in a benchmark scenario 22%) and fewer garbage collection runs. The parameter

-XX:+UnlockExperimentalVMOptions

is no longer necessary (-> to the JEP-519).

JEP-521: Generational Shenandoah

The Shenandoah Garbage Collector is extended by a generational mode, which is intended to improve memory consumption, CPU/energy efficiency and resilience to load peaks - without replacing the proven non-generational variant. In this approach, the Java heap is divided into two generations: a young generation, in which most short-lived objects are collected, and an old generation, which manages long-term objects. The aim is to reduce memory requirements while maintaining low GC pauses.

Since its introduction as an experimental feature in Java 24 (JEP-404), many stability and performance improvements have been implemented and extensive tests carried out. And successfully so that the "experimental" status is now no longer applicable.

Generational mode is activated (together with the Shenandoah GC) as follows:

$ java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational

-> to the JEP-521.

JEP-503: Remove the 32-bit x86 Port

The costs of maintaining the 32-bit port of Java far outweigh the benefits. In Java 24, it was marked as deprecated and removed in a later release (JEP-501). This frees up developer resources to accelerate the development of new features and improvements. In Java 25, the source code and build support for the 23-bit port has now been removed (-> to the JEP-503).

Conclusion

Java 25 also comes with a wealth of new features: APIs that make development more convenient, as well as optimizations "under the hood" in the JVM and the JDK Flight Recorder.

Stefan Waldmann

About ME

Stefan Waldmann has a degree in business informatics and works at doubleSlash as Senior Software Engineer and Lead Developer. In his many years of IT project experience, he has worked, for example, with the Deutsche Telekom AG and the German Post worked together. Among other things, he is an expert in system integration and interfaces as well as Unit testing, test-driven development and software quality. Stefan Waldmann is familiar with many different enterprise software architectures and has extensive experience in the design, integration, development and project management of Java EE projects.

All contributions from Stefan Waldmann

Learn more

Further information on our website and in our newsletter

Arrow up