JDK 25 Key Features — The Fifth LTS, Compact Source Files and Compact Headers

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:

ItemTraditionalJDK 25
Class declarationpublic class Foo { } requiredCan be omitted
main methodpublic static void main(String[] args)void main()
OutputSystem.out.println()IO.println()
Method typestaticInstance 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:

ItemThreadLocalScopedValue
MutabilityMutable (set/get)Immutable (cannot change after binding)
LifecycleRequires explicit remove()Auto-released on block exit
InheritanceRequires InheritableThreadLocalAuto-integrated with StructuredTaskScope
Virtual ThreadsHeavy memory burdenLightweight

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:

MetricImprovement
Heap usage22% reduction
CPU usage8% reduction
GC load15% 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

FeatureStatusKey Points
Compact Source FilesFinalvoid main(), IO.println(), no class declaration needed
Scoped ValuesFinalThreadLocal alternative, immutable, auto-released
Compact Object HeadersFinalHeader 12B -> 8B, heap 22% down, CPU 8% down
Flexible Constructor BodiesFinalValidate arguments before super()
Module ImportFinalimport module java.base;
AOT ImprovementsFinalStartup time 19% improvement
Generational ShenandoahFinalShenandoah 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.

Was this article helpful?