Java 21 to 25 is the smallest LTS-to-LTS step Java has had. Nothing significant was removed from the API, the language changes are additive, and most applications move by changing one number. This post covers what to check anyway.
The one-line version
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
Upgrade build plugins and dependencies first, on 21. Then switch the JDK, run the tests, run the application. For most projects that is the whole exercise.
Use release rather than source and target, as in every other
upgrade. It verifies that you only call APIs that exist in the version you are targeting, which the
older pair does not — with those, code compiles happily against the new JDK's library and then fails
at runtime on an older one. That failure mode is worst precisely when you are shipping a jar someone
else runs.
What actually breaks
1. Bytecode libraries, again. The class-file version goes to 69, so anything reading or generating class files needs a version that understands it — ASM, ByteBuddy, CGLib, and everything built on them. The symptom is the same as every previous upgrade:
java.lang.IllegalArgumentException: Unsupported class file major version 69
The fix is a dependency upgrade. This should get better over time: the Class-File API in the JDK exists precisely so libraries stop needing an urgent release after each LTS.
2. The security manager is permanently gone. Java 17 deprecated it, 18 disabled it
by default, and from 24 it cannot be re-enabled at all. System.setSecurityManager throws
UnsupportedOperationException. If anything still calls it, that is a design change rather
than a flag.
3. String templates, if you previewed them. They were removed in 23. Code using
them will not compile. Rewrite with formatted:
class Demo {
String message(String name, int count) {
return "Hello %s, you have %d orders".formatted(name, count);
}
}
4. Dynamic agent loading. Still a warning, now louder, and heading toward
requiring -XX:+EnableDynamicAgentLoading. Mockito's inline mock maker is the usual
source; upgrading Mockito is the fix.
5. 32-bit x86 is gone. Relevant only on old hardware, but a hard stop if it applies, since there is no 32-bit build to fall back to.
What to check before you commit to it
The realistic blocker is not the JDK, it is your framework. Check that your major dependencies declare Java 25 support rather than assuming forward compatibility works — it usually does, and the exceptions are exactly the bytecode-manipulating libraries above.
# Anything reaching into JDK internals, or built for an old target
jdeps --jdk-internals --multi-release 25 target/myapp.jar
# What is behind
mvn versions:display-dependency-updates
Language changes you can adopt gradually
An upgrade does not require rewriting anything, and it is worth being deliberate about the order. Change the JDK in one commit; adopt features in later ones. A bisect through a commit that does both tells you nothing.
What becomes available between 21 and 25, roughly in order of how likely you are to want it:
| Feature | Standard in | Worth adopting when |
|---|---|---|
Unnamed variables (_) | 22 | immediately — it costs nothing and clarifies ignored values |
| Stream gatherers | 24 | you have a stateful stream operation written as a loop |
| Flexible constructors | 25 | you
have static validator methods feeding super() |
| Module imports | 25 | scripting; rarely in application code |
| Compact source files | 25 | teaching and scratch files, not applications |
| Scoped values | 25 | you use virtual threads and
ThreadLocal |
The first row is the one to take seriously. _ in a catch block that
genuinely ignores its exception is a small, immediate readability win across an entire codebase, and
it needs no design thought.
Things you can turn on afterwards
None of these are required, and each is a separate change from the upgrade itself:
# Smaller object headers — often 10-20% less heap on object-heavy applications
java -XX:+UseCompactObjectHeaders -jar myapp.jar
# Cache startup work between runs — helps short-lived processes most
java -XX:AOTCache=app.aot -jar myapp.jar
Test compact object headers under load before adopting it. It changes no behaviour, but it changes memory layout, and anything measuring heap usage will report different numbers.
Coming from further back
If you are on 8, 11 or 17, do not jump straight here. The disruptive change in the whole range is strong encapsulation, and it lands in 17 — see the Java 17 guide. Getting to 17 first isolates it from everything else, and the hops from 17 to 21 to 25 are each far smaller.
The exception is a small application with few dependencies and good tests, where going straight to 25 is genuinely fine. The number of versions you are skipping matters much less than the number of dependencies you have.
Is it worth it?
Less urgently than any previous LTS upgrade, and that is a good sign rather than a bad one. Java 21 is well supported and will be for years.
Move if you want compact source files for teaching or
scripting, gatherers for a stateful stream operation you
keep hand-rolling, scoped values because ThreadLocal is hurting under virtual threads,
or compact object headers because memory is your constraint.
Otherwise, schedule it rather than rushing it. The most valuable thing about this release is that it does not force anyone's hand — which, for anyone who lived through the Java 8 to 11 migration, is the whole point.
Next
That completes the version-by-version tour. Generics is next, opening the advanced section — topics that are not tied to any one release.