← Back to all articles

Java 27 in Depth: The 9 JEPs and Their Impact

JavaJVMPerformanceSecurity

In one sentence

Java 27 reached General Availability on 15 September 2026 (JSR 402) — the first non-LTS release after Java 25 — and ships nine JEPs. There is no dramatic new syntax; the focus is on three things: firming up runtime defaults (G1 as the default GC everywhere, compact object headers on by default), closing a security gap (post-quantum hybrid key exchange enabled by default in TLS 1.3), and continuing to refine a set of preview and incubating features (lazy constants, primitive pattern matching, structured concurrency, the Vector API and PEM encodings).

For teams still on Java 21 / 25 LTS, Java 27 reads more like a preview of where the platform is heading; but two of its Final changes will greet you anyway the next time you upgrade to an LTS.

Positioning: non-LTS, nine JEPs in three buckets

First, the basics. Oracle ships a feature release every six months and an LTS every two years. Java 21 and Java 25 are LTS releases; Java 27 is not, and Oracle will update JDK 27 until March 2027. Production should still prefer an LTS; Java 27 is best used to validate new behaviour in test environments or to try out on new projects.

MaturityJEPMeaning
Final523 · 527 · 534 · 536Runtime and security changes that are on by default
Preview531 · 532 · 533 · 538Require --enable-preview to use
Incubator537The Vector API keeps incubating; its API may still change

Below we go through language, API, performance, tooling/JVM, and then deprecations and removals, closing with a migration checklist.

Language: two features still in preview

No new syntax was finalised this time; the interesting language work is the evolution of two preview features.

JEP 531 · Lazy Constants (third preview)

This is not a language-level lazy keyword, but a preview API — java.lang.LazyConstant. You provide a computation function at declaration time, and the real initialisation is deferred to the first read; it runs at most once under concurrency, the result is immutable, and it must not be null.

// Not initialised at declaration; created on first get()
private static final LazyConstant<Logger> logger =
    LazyConstant.of(() -> Logger.create(App.class));

// Lazy collections: elements initialised on demand
static final List<Connection> pool =
    List.ofLazy(16, _ -> new Connection());

It targets the "lots of static final fields eager-initialising at startup" problem: ordinary static fields are all evaluated during class initialisation, whereas a lazy constant defers that cost until the value is actually touched. To benefit from the JVM's constant folding, keep the LazyConstant in a final field (ideally static final). Relative to JDK 26, this preview drops the low-level isInitialized() and orElse(T) methods and adds Set.ofLazy(...).

JEP 532 · Primitive types in patterns, instanceof, and switch (fifth preview)

Previously, instanceof, switch and record patterns only worked with reference types. This version lets int, long, float, double, boolean and friends take part in pattern matching, with one central rule: a pattern matches only if the conversion is lossless.

// instanceof with a primitive pattern
int i = 1000;
if (i instanceof byte b) { /* no match: 1000 does not fit a byte */ }

// switch now supports long / boolean, plus guards via when
switch (status) {
    case 0                   -> "ok";
    case int n when n >= 100 -> "high";
    case int n               -> "unknown: " + n;
}

// a primitive pattern nested inside a record pattern
record JsonNumber(double d) {}
if (json instanceof JsonNumber(int age)) { /* only if double → int is lossless */ }

This removes a lot of "cast manually, then range-check" boilerplate, and lets switch handle long, boolean and floating-point selectors directly. Note that a floating-point case constant must match the selector type — a float selector needs 0f, not 0, or it will not compile.

API updates: concurrency, vectors and cryptography

Nothing new was finalised on the API side, but four APIs continue to mature in preview/incubator, alongside a few small final additions.

  • Structured Concurrency (JEP 533, seventh preview): StructuredTaskScope manages a set of related tasks as one unit of work, making parent-child exception propagation, cancellation and observability much clearer — a key piece after virtual threads. Still preview; the API is not yet stable.
  • Vector API (JEP 537, twelfth incubator): a SIMD-oriented API that can compile to CPU vector instructions, aimed at numerical workloads. It has incubated for a long time, so the API may change — do not build production code on it yet.
  • PEM encodings (JEP 538, third preview): read and write certificates and keys in standard PEM form, removing the hand-written "BEGIN/END" glue.
  • Post-quantum hybrid key exchange (JEP 527, final): TLS 1.3 gains X25519MLKEM768, SecP256r1MLKEM768 and SecP384r1MLKEM1024 groups, enabled by default, combining quantum-resistant algorithms with classical ones to defend against "harvest now, decrypt later".

Smaller final updates include KeyStore.getCreationInstant(String) returning an entry's creation time, FFM API support for initialising thread-local execution state before a downcall, new security properties such as jdk.security.password.allowSystemIn, and extra HSS/LMS signature parameter sets.

Performance: G1 everywhere and compact object headers

Two Final changes touch every Java application.

  • JEP 523 · G1 as the default GC in all environments: G1 becomes the default collector everywhere, including client/small-memory cases that previously used Serial GC, unifying default behaviour. G1's MinHeapFreeRatio/MaxHeapFreeRatio defaults also change to 0/100, so the heap no longer shrinks and grows just to satisfy those constraints.
  • JEP 534 · Compact object headers: on 64-bit platforms the object header shrinks from 96 to 64 bits (the class pointer is always compressed), saving more the smaller your heap and the more small objects you allocate. Relatedly, UseCompressedClassPointers is now obsolete — class pointers are always compressed, and setting the flag is ignored with a warning.

For most applications these two defaults are free wins; the libraries to watch are those that depend on object-header layout or do exotic off-heap/bytecode tricks.

Tooling and JVM adjustments

  • JFR in-process data redaction (JEP 536, final): command-line arguments, environment variables and initial system-property values are redacted by default, lowering the risk of writing secrets into JFR recordings; it adds redact-key and redact-argument options, and the jdk.SystemProcess event no longer records command-line arguments.
  • jcmd improvements: Bash completion, plus a VM.security_properties command to inspect a running JVM's security properties; VM.info and the fatal-error log now report the number of open file descriptors.
  • AOT mode: new -XX:AOTMode=required (an alias for on that will replace it).
  • TLS details: TLS 1.3 certificate compression (zlib) is on by default; ffdhe6144 and ffdhe8192 are removed from the default named groups.

Deprecations and removals: check these before upgrading

This is the part to compare line by line when migrating.

Removed

  • ThreadPoolExecutor.finalize() (deprecated earlier)
  • JVMCI: the jdk.internal.vm.ci, jdk.graal.compiler and related modules and flags
  • Launcher options -noclassgc, -noverify, -verifyremote, -Xverify:none
  • The VFORK process-launch mechanism on Linux and the Serviceability Agent printmdo command
  • The java.locale.useOldISOCodes system property (legacy ISO language codes no longer work)

Deprecated / behaviour changes

  • -XX:InitiatingHeapOccupancyPercent is renamed to -XX:G1IHOP (the old name remains an alias)
  • The LDAP provider no longer sets three JNDI standard properties by default; HttpServer moves from string-prefix to path-prefix matching
  • ML-KEM / ML-DSA private keys now default to seed encoding, which has interoperability implications with older JDKs
  • TimeZone.getDefault() returns the latest IANA ID on Windows (e.g. Asia/Kolkata); ServiceLoader throws ServiceConfigurationError consistently on linkage errors

Migration checklist

  1. Decide whether it is worth it: production prefers Java 21 / 25 LTS; use Java 27 in a test environment if you just want to validate new behaviour.
  2. Enable preview explicitly: when using JEP 531/532/533/538, both compilation and runtime need --enable-preview.
  3. Scan for removals: grep for ThreadPoolExecutor.finalize, legacy launcher flags and Graal/JVMCI dependencies; if a library depends on JVMCI, confirm a replacement first.
  4. Audit GC and object-header assumptions: make sure no code or monitoring script depends on the old default GC or on the object-header layout.
  5. Watch cryptographic interoperability: if you exchange ML-KEM/ML-DSA private keys with older versions, note the default encoding is now seed.
  6. Run the full test suite: focus on serialisation, time zones, launch flags and TLS handshakes — the usual trouble spots.

Looking ahead

Zoom out and the longer-term projects are getting closer: Project Valhalla (value types, cutting pointer-chasing and boxing costs), Project Leyden (startup and warm-up performance), and the steady march of post-quantum cryptography. The repeated previews of structured concurrency and the Vector API also show Java will keep polishing both its concurrency and numerical-computing stories. Java 27's value is not "one more syntax sugar" — it is that it hands developers, ahead of time, a batch of behaviours that are about to become the default.

Summary

Java 27 is a foundation release: G1 and compact object headers operate on every byte of the runtime, post-quantum TLS raises the security baseline, and the language/API side stays preview-heavy for teams willing to experiment. For most developers the most important action this cycle is not to upgrade, but to read the two lists — removals and behaviour changes — so the next LTS upgrade is already planned for.