Java 25 vs Java 27: What Changed Between JDK 25 and JDK 27
-
Last Updated: September 27, 2026
-
By: javahandson
-
Series

Java 25 vs Java 27 compared: G1 default GC, compact object headers, lazy constants, post-quantum TLS, support timelines and whether you should upgrade.
Java 25 vs Java 27 is a question many Java developers are asking right now. Java 27 reached general availability on 15 September 2026. However, most teams have only just moved to Java 25, the latest LTS release. So should you jump again, or should you stay where you are?
In this article, I will walk you through every important change between these two versions. We will look at garbage collection, memory, concurrency, security and the language itself. I will also show small code examples, so you can see the difference instead of just reading about it.
By the end, you will know exactly what changed and whether the upgrade makes sense for your project.
If you are short on time, here is the summary first.
In short, Java 27 is a solid step forward. Still, it is not a release you must rush into for production.
Before we compare features, let us understand where each version sits. This part matters more than most people think.
Java 25 became generally available on 16 September 2025. It is an LTS release, just like Java 17 and Java 21 before it.
According to Oracle’s support roadmap, Java 25 gets Premier Support until September 2030. Extended Support goes on until September 2033. That is why most banks, insurance firms and big enterprises pick LTS versions for production.
Java 27 is not an LTS release. Oracle will ship updates for it only until March 2027. After that, you must move to Java 28, and later to Java 29.
The next LTS after Java 25 should be Java 29, expected in September 2027. This follows the usual two-year LTS rhythm.
Here is something many comparison articles skip. When you move from 25 to 27, you also get everything from Java 26. Java 26 came out on 17 March 2026 and brought ten JEPs.
A few Java 26 changes you will feel directly:
HttpClient (JEP 517).final fields (JEP 500).So think of this upgrade as “Java 25 plus two releases of changes”, not just one.

This is one of the most practical changes in Java 27. It can quietly change how your small services behave.
In Java 25, G1 is the default on “server-class” machines. However, the JVM still falls back to the Serial GC on small machines. The rule is simple. If the JVM sees a single CPU, or less than about 1792 MB of memory, it chooses Serial.
This matters a lot in Kubernetes. Many teams run small pods with one CPU and 1 GB of memory. On Java 25, those pods silently run with Serial GC. Serial pauses the whole application during every collection.
JEP 523 removes this special case. Now the JVM always selects G1 when you do not choose a collector yourself. The OpenJDK team explains that recent work made G1 competitive with Serial even on small machines. G1’s throughput is now close to Serial, and its pauses are usually shorter.
You can check which GC your JVM picks with one simple command.
# Simulate a tiny container: 1 CPU and 256 MB heap java -XX:ActiveProcessorCount=1 -Xmx256m -Xlog:gc --version # Java 25 output (small machine): Using Serial # Java 27 output (any machine): Using G1
If your service really performs better with Serial, you can still ask for it directly.
java -XX:+UseSerialGC -jar my-service.jar
A collector you pick on the command line always wins over the JVM’s default choice.
Interview Insight: A common question is “Which GC does Java use by default?” In Java 25, the answer is “G1 on server-class machines, Serial on small ones.” From Java 27 onwards, the answer is simply “G1, in every environment.” Mentioning the old 1-CPU or 1792 MB rule shows the interviewer that you understand container behaviour.
This change saves memory in almost every Java application. You get the benefit without touching a single line of code.
Every Java object carries a small header. The JVM uses it to store the hash code, GC age, lock state and a pointer to the object’s class. On 64-bit systems, this header normally takes 12 bytes (96 bits).
That sounds tiny. But a typical application creates millions of small objects. Those extra bytes add up quickly.
Project Lilliput shrinks the header to 8 bytes (64 bits). The journey happened in three steps:
So on Java 25, you had to enable it manually. On Java 27, you would disable it only if something breaks.
# Java 25: opt in manually java -XX:+UseCompactObjectHeaders -jar app.jar # Java 27: already on. To go back to the old layout: java -XX:-UseCompactObjectHeaders -jar app.jar # Check the current value on any JDK java -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders
For most apps, this is free memory. Services with many small objects, such as DTOs, map entries or event objects, gain the most.
Still, a few things deserve a quick test:
-XX:UseCompressedClassPointers flag is now obsolete in Java 27, because the JVM always compresses class pointers. Remove it from your scripts.Interview Insight: Interviewers often ask how to reduce a Java service’s memory without code changes. Compact Object Headers is a strong answer. Explain that the header drops from 12 bytes to 8 bytes per object. Then add that it is opt-in on Java 25 and the default on Java 27.
Java 25 introduced Stable Values as a preview API (JEP 502). The goal was simple. Give developers a value that behaves like a final field, but the JVM creates it only when first needed.
After feedback, the API got a new name and a smaller shape. In Java 26 it became Lazy Constants (JEP 526). Java 27 continues it as a third preview (JEP 531).
First, here is how lazy logger creation looked in Java 25.
// Java 25 (preview): compile and run with --enable-preview
public class OrderController {
private final StableValue<Logger> logger = StableValue.of();
Logger getLogger() {
return logger.orElseSet(() -> Logger.create(OrderController.class));
}
void submitOrder(User user, List<Product> products) {
getLogger().info("order started");
// ... business logic
getLogger().info("order submitted");
}
}This worked well. However, you still needed a helper method, and the orElseSet() name confused many people.
Now look at the same idea in Java 27.
// Java 27 (third preview): compile and run with --enable-preview
public class OrderController {
private final LazyConstant<Logger> logger =
LazyConstant.of(() -> Logger.create(OrderController.class));
void submitOrder(User user, List<Product> products) {
logger.get().info("order started");
// ... business logic
logger.get().info("order submitted");
}
}Notice the difference. You pass the recipe once, at the declaration. After that, you simply call get(). The first call runs the supplier, and every later call returns the same cached value.
In the third preview, the team removed isInitialized() and orElse(). This keeps the API focused on one job.
Java 27 also gives lazy versions of common collections. In other words, the JVM computes each element only when someone asks for it.
List<Thumbnail> thumbnails = List.ofLazy(pageCount, this::renderThumbnail); Map<String, BigDecimal> rates = Map.ofLazy(currencies, this::fetchRate); Set<String> enabled = Set.ofLazy(candidates, this::isFeatureEnabled);
Set.ofLazy() is new in Java 27. The list and map versions arrived in Java 26.
Remember, this is still a preview feature. So avoid it in production code for now, because the API can change again.
Structured Concurrency lets you treat a group of related tasks as one unit of work. If one task fails, the others get cancelled. As a result, you avoid leaked threads and messy error handling.
It works beautifully with virtual threads. In Java 25, it was in its fifth preview (JEP 505). Java 27 brings the seventh preview (JEP 533).
// Java 25 (fifth preview)
Response handle(long id) throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<User> user = scope.fork(() -> userService.find(id));
Subtask<Order> order = scope.fork(() -> orderService.find(id));
scope.join(); // throws the unchecked FailedException on failure
return new Response(user.get(), order.get());
}
}At first glance, the basic shape looks the same. But a few details changed, and they will affect your code:
join() now throws the familiar checked ExecutionException. Java 27 drops the custom unchecked FailedException.Joiner gained a third type parameter for the exception type.awaitAll() joiner.open() overload lets you configure the scope inline, for example with a timeout and a name.// Java 27 (seventh preview)
Response handle(long id) throws InterruptedException, ExecutionException {
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> userService.find(id));
var order = scope.fork(() -> orderService.find(id));
scope.join(); // now throws the checked ExecutionException
return new Response(user.get(), order.get());
}
}If you tried Structured Concurrency on Java 25, expect small compile errors after the upgrade. Mostly, you will fix throws clauses and catch blocks.
Interview Insight: Interviewers love asking why Structured Concurrency is better than a plain ExecutorService. The key point is lifetime. With an executor, a failed task does not stop its siblings automatically. With StructuredTaskScope, all child tasks finish or get cancelled before the try block ends. Also mention that it is still a preview API in both Java 25 and Java 27.
Pattern matching in Java started with objects. This feature extends it to primitive types like int, long and double.
In Java 25, it was the third preview (JEP 507). Java 27 has the fifth preview (JEP 532). The core idea did not change, but the rules became stricter.
// Preview feature: needs --enable-preview
int score = 82;
String grade = switch (score) {
case int s when s >= 90 -> "excellent";
case int s when s >= 75 -> "good";
default -> "fail";
};
double value = 100;
if (value instanceof byte b) {
// true, because 100 fits in a byte without losing information
System.out.println("Fits in a byte: " + b);
}The instanceof check with a primitive type answers one question. Can this value move to the target type without losing information? So 100 instanceof byte is true, but 0.1 instanceof float is false.
The fifth preview tightens the rules for “unconditional exactness” and dominance in switch. In simple words, the compiler now catches more mistakes where one case hides another. You may see new compile errors in code that compiled on Java 25 with preview enabled.
Java 27 has three security-related changes. Two of them work without any code change, which I really like.
Quantum computers could one day break today’s key exchange algorithms. Attackers can record encrypted traffic now and decrypt it later. That threat is often called “harvest now, decrypt later”.
JEP 527 adds hybrid key exchange to TLS 1.3. It combines a classic algorithm with ML-KEM, a quantum-resistant algorithm that Java 24 introduced. The JDK supports three hybrid groups:
X25519MLKEM768 (enabled and preferred by default)SecP256r1MLKEM768 SecP384r1MLKEM1024
On Java 25, you do not get this protection in TLS. On Java 27, any code that uses the standard javax.net.ssl APIs gets it automatically. That includes HttpClient and most HTTP libraries built on JSSE.
You can still control the groups when a partner system needs something specific.
# Choose the named groups through a system property
java -Djdk.tls.namedGroups="X25519MLKEM768,x25519,secp256r1" -jar app.jar
// Or configure them in code
SSLParameters params = sslSocket.getSSLParameters();
params.setNamedGroups(new String[] {"X25519MLKEM768", "x25519"});
sslSocket.setSSLParameters(params);From my payments integration experience, this is worth testing early. Some older bank gateways and load balancers may not handle the larger key shares well. So test your mTLS partners in a staging environment first.
Loading a PEM key in plain Java has always been painful. You strip headers, decode Base64 and then call a key factory.
The PEM API was a first preview in Java 25 (JEP 470). Java 27 brings the third preview (JEP 538) with a few renames. In Java 25, the raw PEM data came back as a record called PEMRecord. In Java 27, it is a final class called PEM. Also, the DEREncodable interface got a new name, BinaryEncodable.
// Preview API: needs --enable-preview
PrivateKey key = PEMDecoder.of()
.withDecryption(passphrase.toCharArray())
.decode(pemString, PrivateKey.class);
String encoded = PEMEncoder.of()
.withEncryption(passphrase.toCharArray())
.encodeToString(key);JDK Flight Recorder is a great tool for production profiling. However, recordings capture command-line arguments, system properties and environment variables. Those often contain passwords, API keys or tokens.
On Java 25, all of this goes into the .jfr file as plain text. Java 27 (JEP 536) hides values whose names match common secret patterns, such as *password*, *token*, *secret* and *api*key*.
# Add your own pattern on top of the defaults java -XX:FlightRecorderOptions:'redact-key=+*confidential*' -jar app.jar # Turn redaction off completely (not recommended) java -XX:FlightRecorderOptions:'redact-key=none,redact-argument=none' -jar app.jar
Overall, this small change makes it much safer to share recordings with vendors or support teams.
Not everything moved forward. It helps to know what did not change, too.
So your Java 25 code that uses only final features should compile on Java 27 without drama.
Every upgrade removes something. So, between Java 25 and Java 27, check these points carefully:
java.applet will not compile.final fields, you will see warnings at runtime. Serialization and mocking libraries are common suspects.ThreadPoolExecutor.finalize() removed in Java 27. Subclasses that override or call it need a fix.java.locale.useOldISOCodes removed in Java 27. The old language codes like “iw” and “in” no longer come back through this property.-noverify, -Xverify:none, -noclassgc and -verifyremote. Old startup scripts often still carry -noverify, so search for it.-Djdk.lang.Process.launchMechanism=VFORK option no longer works on Linux.Here is a side-by-side view of the most important differences.
| Area | Java 25 | Java 27 |
|---|---|---|
| Release type | LTS (Sept 2025) | Non-LTS (Sept 2026) |
| Oracle Premier Support | Until Sept 2030 | Until March 2027 |
| Default GC on small machines | Serial (1 CPU or under ~1792 MB) | G1 everywhere |
| Compact Object Headers | Available, off by default | On by default |
| Lazy initialization API | StableValue (1st preview) | LazyConstant (3rd preview) |
| Structured Concurrency | 5th preview, FailedException | 7th preview, ExecutionException |
| Primitive patterns | 3rd preview | 5th preview, stricter checks |
| Post-quantum TLS 1.3 | Not available | Hybrid ML-KEM on by default |
| PEM API | 1st preview | 3rd preview |
| JFR secret redaction | No | Yes, by default |
| HTTP/3 in HttpClient | No | Yes (added in Java 26) |
| Vector API | 10th incubator | 12th incubator |
This is the real question. My honest answer depends on where your code runs.
A practical middle path works well for many teams. Keep production on Java 25. Then run your CI build on Java 27 as well. This way, you catch breaking changes early, and the move to Java 29 LTS becomes much easier.
I have seen teams make these mistakes with almost every upgrade. Avoid them and your migration will go smoothly.
–enable-preview at both compile time and runtime. You need it for javac and for java.# Compiling and running preview code on Java 27 javac --release 27 --enable-preview Main.java java --enable-preview Main
A: No. Java 27 is a non-LTS release. Oracle provides updates for it only until March 2027, when Java 28 replaces it. Java 25 is the current LTS, and Java 29 is expected to be the next LTS in September 2027.
A: The biggest practical changes are runtime changes. Java 27 makes G1 the default garbage collector in every environment and turns on Compact Object Headers by default. It also adds post-quantum hybrid key exchange for TLS 1.3 and JFR secret redaction.
A: Java 27 uses G1 by default in all environments (JEP 523). In Java 25, the JVM still chose Serial GC on machines with a single CPU or less than about 1792 MB of memory. You can still select Serial with -XX:+UseSerialGC.
A: No. In Java 25, Compact Object Headers are a product feature, but you must enable them with -XX:+UseCompactObjectHeaders. Java 27 turns them on by default, shrinking the object header from 12 bytes to 8 bytes on 64-bit systems.
A: The Stable Values preview API was renamed to Lazy Constants in Java 26. Java 27 continues it as a third preview with a smaller API: you create a LazyConstant with LazyConstant.of(supplier) and read it with get().
A: No. Java 27 enables the X25519MLKEM768 hybrid key exchange by default for TLS 1.3. Any code using the standard javax.net.ssl APIs benefits automatically. You can still customise the groups with the jdk.tls.namedGroups system property.
A: For most production systems, stay on Java 25 LTS and test Java 27 in CI. Upgrade to Java 27 only if you need its runtime or security gains now and your team is ready to move to Java 28 in March 2027.
The Java 25 vs Java 27 comparison comes down to stability versus fresh improvements. Java 25 is the safe, long-supported base for production. Java 27 gives you real runtime gains, like G1 everywhere and smaller object headers, plus useful security upgrades.
Most preview features, however, are still moving. So treat Java 27 as a great release to test and learn from, rather than something to force into production. If you plan well, you can enjoy these improvements today and land smoothly on Java 29 LTS next year.