Java releases a new LTS version: JDK 25.

It’s worth noting that not long ago, one of the most iconic languages in the industry turned 30 years old. And although it's no longer the fastest-growing language, statistics show that it remains strong and signals a promising future. Who knows — it might be the most mature language by the time it "retires".

This new LTS version follows JDK 24 (released on March 18) and will have Premier support for at least 5 years. The most recent LTS version before this was JDK 21 (September 21, 2023). It’s always good to keep an eye on the support status of the JDK you’re using in case a migration is needed (Oracle support roadmap).

In this post, I’d like to walk through the confirmed features included in this update, as well as a few that are currently in preview but sound interesting enough to keep an eye on — in case they eventually get incorporated.

Here’s the full list of included features in case you'd like to dig deeper.

Confirmed Features

1 Compact Source Files and Instance Main Methods Jeps-512

Java is becoming more user-friendly and simple to use.

void main() {
    IO.println("Hola mundo");
}

It will implicitly generate for us:

final class __ImplicitClass__ {
    void main() { IO.println("Hola mundo"); }
}

You can check the full details in JEP 512.

2 Flexible Constructor Bodies Jeps-513

This feature was previously introduced in preview mode (JDK 22) but is now officially released. It allows adding logic between the default constructor and the object initialization process.

public class A extends B {
    private static int multiplier = 10;
    private final int childValue;

    public A(int baseValue) {
        // Prologue
        int computedValue = baseValue * multiplier;

        super(baseValue); // Initialize B

        // Epilogue
        this.childValue = computedValue;
    }
}

You can check the full details in JEP 513.

3 Ahead-of-Time Method Profiling Jeps-515

When the JVM starts, Java always had to collect data and statistics on methods to decide what to optimize, which causes slow startup times.

Now, we can create an execution profile (most-used methods, how often they run, object types...) and reuse it in later startups. This significantly speeds up the warm-up phase of the application by allowing the JIT compiler to generate optimal native code without waiting for real-time profiling.

It relies on scanning previous executions to be more efficient. AOT (Ahead-of-Time) cache.

The best part? You don't need to change a single line of code — it improves resource usage (about 20% improvement in early runtime).

It’s flexible and adaptive if runtime behavior changes. The JVM continues to collect live profiles, combining AOT + dynamic JIT optimization, ensuring that performance doesn't degrade if the app behaves differently in production.

You can check the full details in JEP 515.

4 Module Import Declarations Jeps-511

Let’s go way back to Java 9 and Project Jigsaw — a major shift in the modular system. This feature introduces a way to import all exported packages from a module with a single line of code.

import module <module-name>;

How does this affect us? We can now use an import like:

import module java.base;

And gain access to all exported packages (List, Map, Path…). Additionally, if the module has a transitive dependency on another, it will also be imported (e.g., java.sql.*, java.xml…). It’s more like using a complete API.

You can read the full details at Jeps-511

5 Scoped Values Jeps-506

This is a modern alternative to ThreadLocal for Virtual Threads (jeps-487). Essentially, it’s a safer way to share data between methods and threads, specifically designed for virtual threads. As we know, ThreadLocal is mutable, can be forgotten (leading to memory leaks), and doesn’t play well with virtual threads.

An example:

public class ScopedValueExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(USER, "Alice").run(() -> {
            greetUser();
        });
    }

    static void greetUser() {
        System.out.println("Hello, " + USER.get());
    }
}

You can find the full information at Jeps-506.

JVM Flags and Optimizations

1 Compact Object Headers Jeps-519

This feature was introduced experimentally in JDK 24 and is now a stable release. It reduces the JVM object headers from 12 bytes to either 16 bytes with uncompressed pointers or 8 bytes. And what does that mean?

As an interesting fact, Amazon reports up to 30% less CPU usage on some services.

To enable it, just add the following flag (it’s not enabled by default):

java -XX:+UseCompactObjectHeaders ...

You can find all the details in Jeps-519.

2 Generational Shenandoah Jeps-521

Just like the previous case, it was introduced in JDK 24 and confirmed in this release, and it changes how the garbage collector works. It allows managing young and old objects separately, improving the efficiency of the garbage collector.

Let’s recall that previously, when the GC kicked in, it had to scan the entire heap. Most objects die young, so the GC now focuses more on these young objects and occasionally scans the old ones.

As with the previous case, it’s not enabled by default and needs to be activated:

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

More info about Jeps-521 at this link.

Preview or Experimental Features

These features are not yet confirmed but are very interesting to keep on the radar.

Structured Concurrency (Fifth Preview) Jeps-505

The goal is to improve the observability of concurrent code and to provide a concurrency style that mitigates risks such as thread cancellation, delays, etc.

This is an API that allows treating groups of related tasks running on different threads as a single unit of work, simplifying error handling and cancellations, improving reliability, and increasing the observability of concurrent code.

import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;

public class StructuredConcurrencyExample {
    public static void main(String[] args) {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure<Void>()) {

            // Fork: we start concurrent tasks
            var task1 = scope.fork(() -> fetchDataFromService1());
            var task2 = scope.fork(() -> fetchDataFromService2());

            // We wait for all of them to finish
            scope.join();       
            scope.throwIfFailed(); // Propaga excepciones si alguna falló

            // We get results
            String result1 = task1.resultNow();
            String result2 = task2.resultNow();

            System.out.println("Resultados:");
            System.out.println("Service1 -> " + result1);
            System.out.println("Service2 -> " + result2);

        } catch (InterruptedException | ExecutionException e) {
            System.err.println("Ocurrió un error en las tareas: " + e.getMessage());
        }
    }

    // Task simulation
    private static String fetchDataFromService1() throws InterruptedException {
        Thread.sleep(1000);
        return "Datos del servicio 1";
    }

    private static String fetchDataFromService2() throws InterruptedException {
        Thread.sleep(1500);
        return "Datos del servicio 2";
    }
}
  1. StructuredTaskScope.ShutdownOnFailure: creates a group of tasks that are automatically cancelled if any of them fails.
  2. fork: starts concurrent tasks and returns a Subtask.
  3. join(): waits for all tasks to complete.
  4. throwIfFailed(): if any task threw an exception, it propagates it.
  5. resultNow(): retrieves the result of the task, since at this point all have finished.

Benefits: the code is clean, easy to read, and cancellation/error propagation is handled automatically, preventing thread leaks.

Full details of Jeps-505 here

Primitive Types in Patterns (Third Preview) Jeps - 507

This feature aims to remove restrictions on the use of primitive types in pattern matching expressions, instanceof, and switch statements. It allows us to use primitive types like int, long, boolean, and others directly in these constructs, providing greater consistency and expressiveness in the language. Additionally, it extends switch support to all primitive types.

An example:

if (obj instanceof int i) {       // directamente con primitivo
    System.out.println("Es un int: " + i);
}
public static String identifyType(Object value) {
    return switch (value) {
            case int i       -> String.format("int: %d", i);   
            case Double d    -> String.format("Double: %f", d);
            case String s    -> String.format("String: %s", s);
            default          -> "Unknown type";
     };
}

Check out all the details of Jeps - 507.

JFR CPU-Time Profiling (experimental) Jeps-509

An experimental feature has been added to improve how CPU usage is measured in a Java program. Until now, only the time spent in “pure Java code” was visible, but with this improvement, time spent in external parts (native code, system libraries, etc.) is also counted. This provides a more accurate view of where CPU time is being consumed.

Note: currently works only on Linux.

Full details on Jeps-509.

Conclusion

Java continues to modernize and deliver value in each new release, optimizing resources and adding useful new features — and even making some of the classic ones more user-friendly.

Long live Java!!

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.

Subscribe