What Changed from Java 21 to 25

See which language, library and runtime features became final between the Java 21 and Java 25 LTS releases, and what to check before you upgrade a service.

Syntaxjava --version # confirm the runtime is 25 javac --release 25 Main.java # compile against the Java 25 API java -XX:+UseCompactObjectHeaders -jar app.jar

Java 21 and Java 25 are both long-term support (LTS) releases. Most teams jump from one LTS to the next, so the four releases in between (22, 23, 24 and 25) arrive as one upgrade. Features that were in preview when you adopted 21 are now final, and several runtime changes help services without any code change at all.

Language and library features that became final

ReleaseFeature
22Unnamed variables and patterns, _ (JEP 456); Foreign Function and Memory API (JEP 454); launching multi-file source programs (JEP 458)
23Markdown documentation comments with /// (JEP 467)
24Stream gatherers (JEP 485); Class-File API (JEP 484)
25Scoped values (JEP 506); module import declarations (JEP 511); compact source files and instance main methods (JEP 512); flexible constructor bodies (JEP 513)

Runtime changes a back-end team notices

  • Virtual threads no longer pin on synchronized (JEP 491, Java 24). Blocking inside a synchronized block used to hold the carrier thread; older JDBC drivers and libraries did exactly that. On 25 the virtual thread unmounts as it would with a ReentrantLock.
  • Generational ZGC became ZGC's default mode in 23, and the non-generational mode was removed in 24.
  • Faster startup through the ahead-of-time cache: class loading and linking (JEP 483, Java 24), a one-step -XX:AOTCacheOutput workflow (JEP 514) and method profiles (JEP 515) in 25.
  • Compact object headers (JEP 519) are a product option in 25, switched on with -XX:+UseCompactObjectHeaders, which cuts heap use for services holding many small objects.
  • The Security Manager is permanently disabled (JEP 486), and the 32-bit x86 port is gone (JEP 503).

What to check before upgrading

Compile with --release 25 so the compiler checks API use against 25. Update anything that reads or writes bytecode: build plugins, mocking libraries, agents and annotation processors must understand class file version 69. Watch the startup log for new warnings: Java 24 warns when code calls the memory-access methods of sun.misc.Unsafe (JEP 498) and when native code is loaded through JNI without being allowed (JEP 472). Those warnings point at libraries you will need to update before they turn into errors.

The common mistake is treating the upgrade as a version bump in the build file only. Run the full test suite, load test the service on the new JDK, and only then start using the new language features.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Planning a 21 to 25 upgrade of a fleet of services by listing which preview features you used and which runtime changes need a load test.
  • Removing <code>ReentrantLock</code> workarounds that were added only to avoid virtual thread pinning in a JDBC-heavy service.
  • Turning on compact object headers and the AOT cache for a service that runs many small container replicas.

More examples

Unnamed variables in catch blocks, lambdas and switch

Final since Java 22, <code>_</code> marks a variable or pattern component you deliberately ignore. Code reviewers can see straight away that the IP address and the caught exception are unused on purpose.

Example · java
import java.util.Map;

sealed interface Event permits Login, Logout, Ping {}
record Login(String user, String ip) implements Event {}
record Logout(String user) implements Event {}
record Ping(long at) implements Event {}

String audit(Event e) {
    return switch (e) {
        case Login(String user, var _) -> "login " + user;
        case Logout(String user) -> "logout " + user;
        case Ping _ -> "ping";
    };
}

void main() {
    IO.println(audit(new Login("ann", "10.0.0.1")));
    IO.println(audit(new Ping(1L)));

    try {
        Integer.parseInt("abc");
    } catch (NumberFormatException _) {
        IO.println("not a number");
    }

    Map<String, Integer> hits = Map.of("/health", 3);
    hits.forEach((_, count) -> IO.println("count " + count));
}
// Output:
// login ann
// ping
// not a number
// count 3

Checking the runtime version at startup

A fail-fast check at startup gives a clear message instead of a confusing <code>UnsupportedClassVersionError</code> or missing method later. Java 25 class files are version 69.

Example · java
void main() {
    Runtime.Version v = Runtime.version();
    int feature = v.feature();
    IO.println("feature release " + feature);
    IO.println("class file major " + System.getProperty("java.class.version"));

    if (feature < 25) {
        IO.println("this service needs Java 25 or newer");
        System.exit(1);
    }
    IO.println("runtime ok");
}
// Output:
// feature release 25
// class file major 69.0
// runtime ok

Blocking inside synchronized on virtual threads

Each task blocks while holding a monitor, the pattern older drivers use. On Java 21 every sleeping task pinned a carrier thread, so on one CPU the calls ran almost one at a time; since Java 24 the virtual threads unmount and the run finishes in well under a second.

Example · java
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

class LegacyClient {
    private final Object lock = new Object();

    int call(int id) throws InterruptedException {
        synchronized (lock) {
            Thread.sleep(20); // stands in for a blocking socket read
            return id;
        }
    }
}

void main() throws Exception {
    AtomicInteger done = new AtomicInteger();
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (int i = 0; i < 2_000; i++) {
            int id = i;
            executor.submit(() -> {
                new LegacyClient().call(id);
                return done.incrementAndGet();
            });
        }
    }
    IO.println("completed " + done.get() + " calls");
}
// Output:
// completed 2000 calls

Discussion

  • Be the first to comment on this lesson.