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.
- You can now skip declaring a class explicitly, and Java will generate one with the same name as the file.
- The main method can now be non-static (although static void main is still supported). When an instance of the class is executed, this method will be run.
void main() {
IO.println("Hola mundo");
}
It will implicitly generate for us:
final class __ImplicitClass__ {
void main() { IO.println("Hola mundo"); }
}
- All public classes from java.base are imported by default. No more need to write import statements for List or ArrayList!
- New class java.lang.IO. The idea is to simplify console interaction, moving away from System.out, Scanner, etc. It includes the following static methods:
- IO.println(Object o)
- IO.println()
- IO.print(Object o)
- IO.readln()
- IO.readln(String prompt)
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.
- Makes it easier to validate arguments before calling the superclass constructor.
- Allows field initialization before they are visible to other methods, enhancing security.
- Removes unnecessary restrictions in constructor code, enabling a more natural and safer way to express construction logic.
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.
- -XX:AOTMode=record → records method and class profiles during execution (not just “used classes”).
- -XX:AOTMode=create → creates the AOT cache file from the recorded profiles.
- -XX:AOTCache=filename.aot → loads the AOT cache in future runs.
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.
- Immutable. This avoids side effects and common ThreadLocal pitfalls.
- Scoped to its execution context (a group of threads):
- Values exist only within the block declared with ScopedValue.where(...).run(...).
- They are automatically cleared after exiting the scope, preventing memory leaks.
- Safe and clean. Unlike ThreadLocal, they work flawlessly with virtual threads, as they flow with the execution context, not a physical thread.
- Simple. Accessed via get(), similar to ThreadLocal.get(), but safe and clean.
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?
- Lower heap usage: in some cases, a 22% reduction (SPECjbb2015 benchmark)
- Lower CPU usage: 8% (SPECjbb2015)
- More efficient garbage collection
- Faster JSON processing
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";
}
}
- StructuredTaskScope.ShutdownOnFailure: creates a group of tasks that are automatically cancelled if any of them fails.
- fork: starts concurrent tasks and returns a Subtask.
- join(): waits for all tasks to complete.
- throwIfFailed(): if any task threw an exception, it propagates it.
- 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.
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.
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!!
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.
Tell us what you think.