JDK 27 Is Here: What You Get Just by Upgrading (and What Needs --enable-preview)
JDK 27 went GA on 15 September 2026 with 9 JEPs. G1 everywhere, compact object headers, post-quantum TLS and JFR redaction change behaviour with zero code changes; lazy constants, primitive patterns, structured concurrency and the PEM API are previews. What each one means, with working Java examples.
JDK 27 reached General Availability on 15 September 2026. It contains 9 JEPs: 4 that change the runtime by default, 4 preview features and 1 incubator API.
The interesting part of this release isn't new syntax. It's how much improves when you change nothing but the JDK: garbage collector choice, object memory layout, TLS key exchange and diagnostics.
Here's the whole release, grouped by what you have to do to benefit.
| JEP | Feature | Type | Action needed |
|---|---|---|---|
| 523 | G1 is the default GC in all environments | Default change | None |
| 534 | Compact object headers by default | Default change | None |
| 527 | Post-quantum hybrid key exchange for TLS 1.3 | New default | None |
| 536 | JFR in-process data redaction | New default | None |
| 531 | Lazy constants (third preview) | Preview | --enable-preview |
| 532 | Primitive types in patterns, instanceof and switch (fifth preview) | Preview | --enable-preview |
| 533 | Structured concurrency (seventh preview) | Preview | --enable-preview |
| 538 | PEM encodings of cryptographic objects (third preview) | Preview | --enable-preview |
| 537 | Vector API (twelfth incubator) | Incubator | --add-modules jdk.incubator.vector |
Part 1: Free improvements β upgrade and they apply
G1 everywhere (JEP 523)
G1 has been the default collector on "server-class" machines since JDK 9. But the JVM used to pick Serial GC when it detected a constrained environment: a single CPU or less than 1792 MB of physical memory. That's exactly what many small containers, CI runners and command-line tools look like.
From JDK 27, the JVM always picks G1 unless you choose a collector yourself.
Why the switch is safe now:
- Throughput: G1's throughput has caught up with Serial, helped by the synchronization reductions delivered in JDK 26 (JEP 522).
- Latency: G1 collects the old generation incrementally, so its worst-case pauses have always been better than Serial's full collections.
- Memory: G1's native memory overhead is now comparable to Serial's.
What it means for you:
- A small container that silently ran on Serial GC will run on G1 after upgrading. Expect shorter worst-case pauses; measure memory and throughput anyway.
- To keep Serial for a specific workload:
-XX:+UseSerialGC. - Interview favourite: "Which GC does the JVM use by default?" The accurate answer is now "G1, in every environment, since JDK 27 β before that, Serial on single-CPU or low-memory machines."
Compact object headers (JEP 534)
Every Java object starts with a header. On 64-bit JVMs it has been 96 bits (12 bytes): a 64-bit mark word (hash code, GC age, lock state) plus a 32-bit compressed class pointer.
Compact object headers fold the class pointer into the mark word, giving a 64-bit (8-byte) header β a third smaller. This shipped as experimental in JDK 24 (JEP 450), became a supported opt-in in JDK 25, and is on by default in JDK 27.
Because typical applications allocate huge numbers of small objects, header size adds up. Published results for the feature include:
- SPECjbb2015: 22% less heap and 8% less CPU time
- 15% fewer garbage collections with G1 and Parallel GC
- A highly parallel JSON parsing benchmark ran 10% faster
Your mileage depends on object sizes: the savings are largest for applications dominated by small objects (DTOs, collections of boxed values, graph structures).
Things to check:
- Tools that inspect raw object layout (some profilers, agents, memory analysers) may need updated versions.
- To go back to the old layout:
-XX:-UseCompactObjectHeaders. The legacy layout is expected to be deprecated in a future release.
Post-quantum TLS 1.3 (JEP 527)
Attackers can record encrypted traffic today and try to decrypt it years later if large quantum computers arrive ("harvest now, decrypt later"). The defence is to use quantum-resistant key exchange now.
JDK 27's TLS 1.3 implementation adds three hybrid key-exchange groups, each combining a classic elliptic-curve exchange with ML-KEM (the NIST-standardised post-quantum KEM). The session is safe as long as either part holds:
| Group | Combines | Enabled by default? |
|---|---|---|
X25519MLKEM768 | X25519 + ML-KEM-768 | Yes β and preferred first |
SecP256r1MLKEM768 | secp256r1 + ML-KEM-768 | No |
SecP384r1MLKEM1024 | secp384r1 + ML-KEM-1024 | No |
Two details that matter:
- Both sides must support it. Your JDK 27 client offers
X25519MLKEM768first, but you only get a post-quantum session if the server (or client, for your servers) supports the same group; otherwise TLS falls back to a classic group. - You can control it with the
jdk.tls.namedGroupssystem property, or per connection withSSLParameters.setNamedGroups(...):
SSLParameters params = sslSocket.getSSLParameters();
params.setNamedGroups(new String[] {
"X25519MLKEM768", "SecP256r1MLKEM768", "x25519", "secp256r1" });
sslSocket.setSSLParameters(params);
JFR redaction (JEP 536)
JDK Flight Recorder recordings capture command-line arguments, environment variables and system properties β which is exactly where secrets end up (--db-password=..., ACCESS_TOKEN, javax.net.ssl.keyStorePassword).
From JDK 27, JFR redacts sensitive values by default, replacing them with [REDACTED] before the data leaves the process. Default filters match names such as *password*, *token*, *secret*, *credential* and *api*key*.
You can extend or change the filters:
-XX:FlightRecorderOptions:redact-key=+*pin*
The + adds your pattern to the defaults instead of replacing them; none disables redaction. A separate redact-argument option filters command-line arguments. This makes it much safer to attach a production recording to a support ticket.
Part 2: Preview features β try them, don't ship them yet
Preview features are complete but may still change. Enable them explicitly:
javac --release 27 --enable-preview Main.java
java --enable-preview Main
Lazy constants (JEP 531)
static final fields are initialised when the class is initialised β even if the value is expensive and rarely used. Hand-written lazy initialisation (double-checked locking, holder classes) works, but the JVM can't treat the result as a true constant.
A lazy constant is computed at most once, on first use, and then the JVM can optimise it like a final field:
public class PricingEngine {
// Loaded on first use, never again, and treated as a constant afterwards.
private static final LazyConstant<TaxRules> TAX_RULES =
LazyConstant.of(() -> TaxRules.loadFrom(Path.of("tax-rules.json")));
public Money priceWithTax(Money net, String region) {
return TAX_RULES.get().apply(net, region);
}
}
There are lazy collections too, where each element is computed on first access β List.ofLazy, Map.ofLazy and, new in this preview, Set.ofLazy:
// One expensive client per region, created only when that region is first used.
static final Map<String, PaymentClient> CLIENTS =
Map.ofLazy(Set.of("IN", "EU", "US"), PaymentClient::forRegion);
This third preview also removed the low-level isInitialized() and orElse(...) methods.
Primitive types in patterns (JEP 532)
Pattern matching now works with primitive types in instanceof and switch. The neat part is that a primitive pattern checks whether a conversion is exact (no loss of information):
long fileSize = Files.size(path);
if (fileSize instanceof int size) {
// only runs if the long value fits in an int without loss
buffer = new byte[size];
} else {
streamInChunks(path);
}
switch can also use primitive patterns with guards:
String describeRetry(int attempt) {
return switch (attempt) {
case 0 -> "first try";
case int n when n <= 3 -> "retry " + n;
case int n -> "giving up after " + n;
};
}
Structured concurrency (JEP 533)
When a request fans out into several concurrent subtasks, plain ExecutorService code has no idea they belong together: if one fails, the others keep running; if the caller is interrupted, the subtasks may leak.
Structured concurrency ties subtasks to a scope that has a clear start and end. The API has changed a lot across previews β code using the old ShutdownOnFailure class from earlier previews no longer compiles. The current style uses StructuredTaskScope.open():
OrderSummary loadSummary(long orderId) throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<Order> order = scope.fork(() -> orderService.find(orderId));
Subtask<Shipment> shipment = scope.fork(() -> shippingService.track(orderId));
scope.join(); // waits for both; if one fails, the other is cancelled
return new OrderSummary(order.get(), shipment.get());
} // leaving the scope guarantees no subtask is still running
}
Custom policies come from joiners, for example collecting every result or failing on the first error:
try (var scope = StructuredTaskScope.open(Joiner.<Quote>allSuccessfulOrThrow())) {
suppliers.forEach(s -> scope.fork(() -> s.quote(item)));
return scope.join(); // results of all subtasks, or an exception
}
New in the seventh preview: an open(...) overload that takes a configuration function (scope name, timeout, thread factory), a new type parameter for the exception join() throws, and timeouts reported as an ExecutionException caused by CancelledByTimeoutException. Structured concurrency pairs naturally with virtual threads, which make forking a subtask per call cheap.
PEM encodings (JEP 538)
PEM β the -----BEGIN ...----- text format β is how keys and certificates are usually exchanged, but Java has never had a simple API for it. This preview adds PEMEncoder and PEMDecoder:
String pem = PEMEncoder.of().encodeToString(keyPair.getPublic());
PublicKey key = PEMDecoder.of().decode(pem, PublicKey.class);
// Encrypted private keys
String protectedPem = PEMEncoder.of().withEncryption(password).encodeToString(privateKey);
No more hand-written Base64 stripping of BEGIN/END lines. The third preview renamed DEREncodable to BinaryEncodable, turned PEM into a regular class and added an unchecked CryptoException.
Part 3: Incubator β the Vector API (JEP 537)
The Vector API lets Java code express SIMD operations β one CPU instruction processing several values β which the JIT compiles to instructions such as AVX on x64 or NEON/SVE on ARM. It matters for numeric work: similarity search over embeddings, image processing, signal processing, analytics.
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
static float dot(float[] a, float[] b) {
var sum = FloatVector.zero(SPECIES);
int i = 0;
for (; i < SPECIES.loopBound(a.length); i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
sum = va.fma(vb, sum); // sum += va * vb, lane by lane
}
float result = sum.reduceLanes(VectorOperators.ADD);
for (; i < a.length; i++) { // leftover elements
result += a[i] * b[i];
}
return result;
}
Run with --add-modules jdk.incubator.vector. It's in its twelfth incubation: the API is waiting on value types from Project Valhalla before it can become final, so expect it to keep changing.
Should you upgrade?
JDK 27 is not an LTS release. JDK 25 (September 2025) is the current LTS; non-LTS releases get updates only until the next feature release six months later, and the next LTS is expected two years after JDK 25.
A sensible approach:
- Production on JDK 21 or 25: stay on your LTS. Several JDK 27 changes (G1 everywhere, compact headers) matter mostly if you're moving off an older default anyway β and you can already opt in to compact headers on JDK 25 with
-XX:+UseCompactObjectHeaders. - New services, internal tools, experiments: JDK 27 is a good way to get ahead of the next LTS.
- Learning: try structured concurrency, lazy constants and primitive patterns now β they're heading towards the next LTS and they'll show up in interviews.
Upgrade checklist
- Run your test suite and a load test on JDK 27 β watch heap, GC pauses and CPU.
- Small containers: confirm the switch from Serial to G1 suits you (or pin
-XX:+UseSerialGC). - Update profilers, agents and memory tools that read object layouts.
- TLS: check that load balancers, proxies and partners handle the larger post-quantum key share in the TLS handshake; pin
jdk.tls.namedGroupsif something misbehaves. - Don't enable preview features in production builds.
Follow-up questions this topic invites β and their answers
Q: What is the default garbage collector in JDK 27? A: G1, in every environment. Before JDK 27 the JVM chose Serial GC on machines with a single CPU or less than 1792 MB of memory, and G1 otherwise (since JDK 9).
Q: What are compact object headers and why do they help? A: They shrink the object header on 64-bit JVMs from 96 to 64 bits by folding the compressed class pointer into the mark word. Smaller objects mean less heap, fewer GCs and better cache use β for example 22% less heap and 8% less CPU on SPECjbb2015.
Q: How do you turn compact object headers off?
A: With -XX:-UseCompactObjectHeaders. On JDK 25 it was opt-in with -XX:+UseCompactObjectHeaders.
Q: Does JDK 27 make all my HTTPS connections quantum-safe?
A: No. It offers the hybrid X25519MLKEM768 group first by default, but a connection only uses it if the other side supports it too; otherwise TLS negotiates a classic group. The other two hybrid groups must be enabled explicitly.
Q: What does "hybrid" key exchange mean? A: It combines a classic elliptic-curve exchange with the post-quantum ML-KEM algorithm, so the session stays secure as long as either one remains unbroken.
Q: What does JFR redaction protect?
A: Values of command-line arguments, environment variables and system properties whose names match sensitive patterns (password, token, secret, credential, API key). They're replaced with [REDACTED] in recordings by default.
Q: How is a lazy constant different from a static final field?
A: A static final is computed during class initialisation even if never used; a lazy constant is computed at most once on first access, is thread-safe, and can still be optimised by the JVM like a constant.
Q: What does long x instanceof int i do with primitive patterns?
A: It matches only if the value converts to int exactly, without losing information, and binds it to i.
Q: Why is structured concurrency better than submitting tasks to an executor? A: Subtasks are tied to a scope: when one fails the others are cancelled, the parent waits for all of them, and no subtask can outlive the scope. Errors and cancellation propagate predictably, and thread dumps show the task hierarchy.
Q: Is StructuredTaskScope.ShutdownOnFailure still available?
A: No β that class belonged to earlier previews. Current previews use StructuredTaskScope.open() with optional joiners such as Joiner.allSuccessfulOrThrow().
Q: Why is the Vector API still incubating after so many releases? A: It's waiting for value classes from Project Valhalla so vectors can be represented efficiently; until then the API stays in incubation and may change.
Q: Should I move production to JDK 27? A: Usually not β it's a non-LTS release with a six-month update window. Stay on JDK 21 or 25 in production, and use JDK 27 for new or internal services and for learning the features heading to the next LTS.
Related Posts
Java Garbage Collection Explained: G1, ZGC and How to Choose
How the JVM finds garbage, why generations matter, what G1 and ZGC actually do, and a practical way to pick and tune a collector for your service.
Virtual Threads in Java 21: When They Help and When They Don't
Virtual threads make blocking code scale to hundreds of thousands of concurrent tasks β but only for I/O-bound work. How they work, how to use them in Spring Boot, and the pitfalls.
HashMap Internals: How put() and get() Really Work
Buckets, hash spreading, collisions, treeification and resizing β a step-by-step look inside java.util.HashMap, and why equals() and hashCode() must agree.
10 Java Output Questions That Trip Up Experienced Developers
Integer caching, the String pool, finally blocks that override returns β ten short Java snippets where intuition gives the wrong answer, with the exact reason for each.