The Fifth LTS
JDK 25, scheduled for release on September 16, 2025, is the fifth LTS (Long-Term Support) release. In the LTS lineage of JDK 8 -> 11 -> 17 -> 21 -> 25, JDK 25 is where many key features that have gone through preview stages become finalized en masse.
In particular, Compact Source Files (JEP 512) fundamentally lowers Java’s entry barrier, and Compact Object Headers (JEP 519) dramatically improves JVM memory efficiency. As befitting an LTS version, changes worth considering for production migration are concentrated here.
Compact Source Files and Instance Main Methods (JEP 512, Final)
The biggest hurdle when first learning Java was having to memorize boilerplate like public class, public static void main(String[] args) just to print “Hello World.” JDK 25 resolves this.
// HelloJdk25.java — JDK 25 Compact Source File
// No class declaration needed, no static — directly executable
void main() {
// IO.println() is a concise alternative to System.out.println()
IO.println("Hello, JDK 25!");
// Output: Hello, JDK 25!
// Variable declaration and usage without a class
var languages = java.util.List.of("Java", "Kotlin", "Scala");
IO.println("JVM languages: " + languages);
// Output: JVM languages: [Java, Kotlin, Scala]
// Instance method, so this reference is available
greet("developer");
}
void greet(String name) {
IO.println("Welcome, " + name + "!");
// Output: Welcome, developer!
}
The difference compared to the traditional approach is stark:
// Traditional way (before JDK 25)
public class HelloTraditional {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
// JDK 25 way
// HelloCompact.java
void main() {
IO.println("Hello, Java!");
}
Here’s a summary of the key changes:
| Item | Traditional | JDK 25 |
|---|---|---|
| Class declaration | public class Foo { } required | Can be omitted |
| main method | public static void main(String[] args) | void main() |
| Output | System.out.println() | IO.println() |
| Method type | static | Instance method |
This feature isn’t just for education. It’s also useful for rapid prototyping and script-like utility writing. Of course, the traditional public static void main(String[] args) approach remains fully compatible.
Scoped Values (JEP 506, Final)
ThreadLocal has long been used as Java’s per-thread data store, but it has several fundamental issues. If remove() isn’t called after setting a value, memory leaks occur, and inheriting values to child threads can cause unexpected behavior. In Virtual Thread environments where millions of threads are created, ThreadLocal’s memory issues become even more severe.
Scoped Values solve this with immutability and scope limitation.
import java.util.concurrent.StructuredTaskScope;
public class ScopedValueDemo {
// ScopedValue: immutable, scope-limited, auto-released
private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public static void main(String[] args) throws Exception {
// Bind values with ScopedValue.where() — auto-released when run() block ends
ScopedValue.where(CURRENT_USER, "John Doe")
.where(REQUEST_ID, "REQ-2025-001")
.run(() -> {
System.out.println("Current user: " + CURRENT_USER.get());
System.out.println("Request ID: " + REQUEST_ID.get());
// Output:
// Current user: John Doe
// Request ID: REQ-2025-001
// Accessible from downstream methods too
processRequest();
});
// Outside the scope, value doesn't exist
System.out.println("Outside scope — binding exists? " + CURRENT_USER.isBound());
// Output: Outside scope — binding exists? false
}
static void processRequest() {
// Auto-propagated along the method call chain — no parameter passing needed
String user = CURRENT_USER.get();
String reqId = REQUEST_ID.get();
System.out.println("[" + reqId + "] Processing request for " + user + "...");
// Output: [REQ-2025-001] Processing request for John Doe...
}
}
Here’s a comparison of ThreadLocal vs ScopedValue:
| Item | ThreadLocal | ScopedValue |
|---|---|---|
| Mutability | Mutable (set/get) | Immutable (cannot change after binding) |
| Lifecycle | Requires explicit remove() | Auto-released on block exit |
| Inheritance | Requires InheritableThreadLocal | Auto-integrated with StructuredTaskScope |
| Virtual Threads | Heavy memory burden | Lightweight |
Compact Object Headers (JEP 519, Final)
Every Java object has an object header attached. This metadata area stores GC info, hash code, lock state, etc. — previously occupying 12 bytes (64-bit JVM, Compressed Oops).
JDK 25 compresses this header to 8 bytes. While 4 bytes less may seem trivial, the effect is cumulative since it applies to every object running on the JVM.
import java.util.ArrayList;
import java.util.List;
public class CompactHeadersDemo {
// Smaller objects have a higher header ratio, so the effect is greater
record Point(int x, int y) {} // Object size: header(8B) + int(4B) + int(4B) = 16B
public static void main(String[] args) {
// Create 1 million Point objects
Runtime runtime = Runtime.getRuntime();
runtime.gc(); // Run GC to establish baseline
long beforeMemory = runtime.totalMemory() - runtime.freeMemory();
List<Point> points = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
points.add(new Point(i, i * 2));
}
long afterMemory = runtime.totalMemory() - runtime.freeMemory();
long usedMB = (afterMemory - beforeMemory) / (1024 * 1024);
System.out.println("=== Compact Object Headers Effect ===");
System.out.println("Objects created: " + points.size());
System.out.println("Memory used: ~" + usedMB + " MB");
System.out.println();
// Before JDK 25: header 12B + fields 8B = 20B -> padded to 24B (alignment)
// JDK 25: header 8B + fields 8B = 16B -> no padding
long oldEstimate = 1_000_000L * 24 / (1024 * 1024);
long newEstimate = 1_000_000L * 16 / (1024 * 1024);
System.out.println("Old estimate (header 12B): ~" + oldEstimate + " MB");
System.out.println("JDK 25 estimate (header 8B): ~" + newEstimate + " MB");
System.out.println("Savings: ~" + ((oldEstimate - newEstimate) * 100 / oldEstimate) + "%");
// Example output:
// === Compact Object Headers Effect ===
// Objects created: 1000000
// Memory used: ~15 MB
//
// Old estimate (header 12B): ~22 MB
// JDK 25 estimate (header 8B): ~15 MB
// Savings: ~31%
}
}
Official figures measured against the SPECjbb benchmark:
| Metric | Improvement |
|---|---|
| Heap usage | 22% reduction |
| CPU usage | 8% reduction |
| GC load | 15% reduction |
These performance improvements come simply by upgrading to JDK 25 with no code changes required.
Flexible Constructor Bodies (JEP 513, Final)
Previously, constructors in Java required super() or this() as the first statement. To validate arguments before calling the parent constructor, you had to create a separate static factory method.
In JDK 25, you can write validation code before super() calls.
import java.util.Objects;
public class FlexibleConstructorDemo {
// Parent class
static class Animal {
final String name;
final int age;
Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Animal created: " + name + " (age " + age + ")");
}
}
// JDK 25: Validation code can be written before super() call
static class Dog extends Animal {
final String breed;
Dog(String name, int age, String breed) {
// Validate arguments before super() — was a compile error before JDK 25!
Objects.requireNonNull(name, "Name is required");
Objects.requireNonNull(breed, "Breed is required");
if (age < 0 || age > 30) {
throw new IllegalArgumentException("Age must be between 0-30: " + age);
}
// Call parent constructor after validation passes
super(name, age);
this.breed = breed;
System.out.println("Dog created: breed=" + breed);
}
}
public static void main(String[] args) {
// Normal creation
Dog dog = new Dog("Buddy", 5, "Labrador");
System.out.println(dog.name + " / " + dog.breed);
// Output:
// Animal created: Buddy (age 5)
// Dog created: breed=Labrador
// Buddy / Labrador
// Invalid arguments — validated before super() call
try {
new Dog("Rex", -1, "Shiba");
} catch (IllegalArgumentException e) {
System.out.println("Validation failed: " + e.getMessage());
// Output: Validation failed: Age must be between 0-30: -1
}
}
}
Previously, workarounds like static factory methods or embedding validation logic inside super() arguments using ternary operators were needed. JDK 25 makes “validate first, initialize later” possible naturally.
Other Notable JEPs
Module Import Declarations (JEP 511, Final): The import module syntax that started as a preview in JDK 23 is now finalized. A single import module java.base; line imports all packages including java.util, java.io, java.time, etc.
AOT Simplification + Method Profiling (JEP 514/515): AOT (Ahead-of-Time) class loading setup is simplified, and method profiling data is leveraged to improve startup time by approximately 19%.
Generational Shenandoah (JEP 518, Final): Following ZGC, Shenandoah GC’s generational mode is also finalized. The low-latency GC options are now richer.
Key Derivation Function API (JEP 510, Final): A standard API for key derivation functions like HKDF was added. Continuing the modernization of Java’s security APIs alongside quantum-resistant cryptography (JDK 24).
Summary
| Feature | Status | Key Points |
|---|---|---|
| Compact Source Files | Final | void main(), IO.println(), no class declaration needed |
| Scoped Values | Final | ThreadLocal alternative, immutable, auto-released |
| Compact Object Headers | Final | Header 12B -> 8B, heap 22% down, CPU 8% down |
| Flexible Constructor Bodies | Final | Validate arguments before super() |
| Module Import | Final | import module java.base; |
| AOT Improvements | Final | Startup time 19% improvement |
| Generational Shenandoah | Final | Shenandoah GC generational mode |
JDK 25, as an LTS version, will be the production environment standard for years to come. Compact Object Headers alone provide sufficient upgrade value, and Scoped Values and Flexible Constructor Bodies offer immediate code quality improvements. If you’re currently on JDK 21 LTS, it’s recommended to start planning your JDK 25 LTS migration now.