Java 25 Other Improvements

August 8, 20264 min readUpdated 8/20/2026

Java 25 is the current LTS. The features with their own posts — compact source files, gatherers and flexible constructors — are the visible ones. This collects what else arrived between 21 and 25.

Scoped values

A replacement for ThreadLocal, and the reason it exists is virtual threads. A ThreadLocal is fine with two hundred threads and a memory problem with a million:

class Context {
    // The old way: mutable, inherited by child threads, and freed only when the
    // thread dies — which for a pooled thread may be never.
    private static final ThreadLocal<String> USER = new ThreadLocal<>();

    void handle(String user) {
        USER.set(user);
        try {
            process();
        } finally {
            USER.remove();          // forget this and you have a leak
        }
    }

    void process() {
        System.out.println(USER.get());
    }
}

A scoped value is immutable and bound for a defined region, so there is nothing to remove and no way to leak it — the binding ends when the block does:

// ScopedValue, standard in Java 25
// private static final ScopedValue<String> USER = ScopedValue.newInstance();
//
// void handle(String user) {
//     ScopedValue.where(USER, user).run(this::process);
// }                                   // binding ends here, automatically
//
// void process() {
//     System.out.println(USER.get());
// }

The other gain is that it is cheap to share with child threads, which matters when a request fans out across many virtual threads and each needs the same request id or user.

Compact object headers

java -XX:+UseCompactObjectHeaders -jar myapp.jar

Every Java object carries a header. Shrinking it from 12 bytes to 8 sounds trivial until you consider an application holding tens of millions of small objects — reported heap reductions are commonly in the 10-20% range, with a corresponding drop in GC work.

It is opt-in in 25 and expected to become the default. Worth testing if memory is your constraint; it changes no behaviour, only layout.

Structured concurrency

Still previewing, and worth watching. It gives a task that spawns subtasks a defined lifetime: if one subtask fails, its siblings are cancelled, and the parent cannot exit before they finish.

That is the guarantee unstructured thread-spawning never gave you. Virtual threads made fanning out cheap; structured concurrency is what makes it safe. Together they are the argument for the pair of releases.

Smaller additions

class Demo {
    void run() {
        // Java 22: Math.clamp had arrived in 21; the ceilDiv family rounds out integer maths
        System.out.println(Math.ceilDiv(7, 2));        // 4 — not 3
        System.out.println(Math.floorDiv(-7, 2));      // -4 — not -3
        System.out.println(Math.clamp(150, 0, 100));   // 100

        // Character and String remain the most-added-to classes
        System.out.println("hello".indexOf('l', 0, 5));   // 2 — bounded search
    }
}

ceilDiv is more useful than it looks: computing "how many pages of size 20" is Math.ceilDiv(total, 20), replacing the (total + 19) / 20 idiom that is correct and unreadable.

The Class-File API

Standard in 24, and it matters more than its audience size suggests. The JDK now has its own API for reading and writing class files, replacing the ASM library that almost every framework bundled a copy of:

// java.lang.classfile — read a class file without a third-party library
// ClassModel model = ClassFile.of().parse(Path.of("Target.class"));
// for (MethodModel method : model.methods()) {
//     System.out.println(method.methodName().stringValue());
// }

The problem it solves is one you have probably hit indirectly. Every LTS bumps the class-file version, and every library doing bytecode work needed a new ASM release to understand it — which is exactly why Unsupported class file major version is the most common failure in a Java 21 upgrade.

With the API in the JDK, it is versioned with the JDK and cannot lag behind it. You will not call it directly; you benefit when Spring, Hibernate and Mockito stop needing an urgent release after each LTS.

Vector API, still previewing

Now in its umpteenth preview round, the Vector API expresses computations that compile to SIMD instructions — doing arithmetic on eight floats at once rather than one. It is waiting on Project Valhalla, so it has been previewing for years and will continue to. Relevant to numerical and image-processing code, and safely ignorable otherwise.

What was removed or restricted

  • The security manager is permanently disabled. It cannot be re-enabled from Java 24 onwards. Anything depending on it needs a different design.
  • String templates are gone — previewed in 21, removed in 23. Do not adopt them; see the Java 21 notes.
  • 32-bit x86 support is gone. Only relevant if you are on very old hardware.
  • Dynamic agent loading warns loudly and moves closer to requiring a flag.

Performance, without changing anything

Worth stating because it is the least discussed reason to upgrade. Between 21 and 25 there have been steady improvements to the collectors, to string handling, to the JIT's escape analysis, and to startup time.

Ahead-of-time class loading and linking, added in 24, caches the work done during JVM startup so a subsequent run skips it. For a short-lived process — a CLI tool, a serverless function — that is a meaningful fraction of total runtime, and it needs no code changes.

Should you be on 25?

For a new project, yes — start on the current LTS. For an existing one, the honest answer is that 21 is a perfectly good place to be and the gap to 25 is smaller than any previous LTS-to-LTS step.

The arguments for moving are compact object headers if memory is tight, scoped values if you have adopted virtual threads and are fighting ThreadLocal, and the support window. The argument against is simply that 21 is well supported and there is no urgency. That is a much more comfortable position than the one Java 8 users were in.

Next

The Java 25 migration guide is next.