JDK 27 went GA on Tuesday, September 15, as build 27+35. Mark Reinhold’s announcement lists nine JEPs, the fewest since JDK 20 shipped seven. JDK 26 had ten and JDK 25 had eighteen.
Where I could run the change on the GA build instead of describing it, I did. Every terminal screenshot below is real output from build 27+35 on an Apple Silicon Mac.
If you prefer your releases narrated, the Java 27 Launch Stream went out on the Java channel on release day.
But if you want my -let’s unpack what’s inside together!
1. JDK 27 is out, and here is what is in it
Cryptography Updates
JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3
If there’s a single reason to pay attention to JDK 27, this is it. JEP 527 brings post-quantum cryptography into the TLS handshake itself, and, crucially, it does so by default, with zero code changes on your side.
The problem it solves: quantum computers capable of breaking RSA and Elliptic-Curve Diffie-Hellman don’t exist yet. But that’s cold comfort, because of the wonderfully ominous “harvest now, decrypt later” threat model: an adversary records your encrypted traffic today, sits on it, and decrypts it the day a sufficiently large quantum computer shows up. For anything that needs to stay secret for years (medical records, state secrets, that embarrassing Slack export), the clock is already ticking.
The solution: the IETF TLS Working Group settled on hybrid key exchange: combine a battle-tested classical algorithm with a quantum-resistant one, and you’re safe as long as either survives. This hedges against quantum attacks while acknowledging that the new lattice-based algorithms haven’t yet had decades of cryptanalysis thrown at them. JDK 27 implements three such schemes, each pairing ML-KEM with Ephemeral Elliptic-Curve Diffie-Hellman (ECDHE):
X25519MLKEM768: X25519 + ML-KEM-768
SecP256r1MLKEM768: secp256r1 + ML-KEM-768
SecP384r1MLKEM1024: secp384r1 + ML-KEM-1024
This is the natural next step in a multi-release journey: the KEM API arrived in Java 21 (JEP 452), the ML-KEM algorithm itself in Java 24 (JEP 496), and now JDK 27 wires them into TLS 1.3.
The best part: TLS clients advertise their supported “named groups” in preference order during the handshake, and X25519MLKEM768 goes straight to the front of that list. So any code using 𝚓𝚊𝚟𝚊𝚡.𝚗𝚎𝚝.𝚜𝚜𝚕 gets quantum-resistant handshakes automatically, provided it doesn’t already pin specific named groups. If you do want to take control, you can override the default list via the 𝚓𝚍𝚔.𝚝𝚕𝚜.𝚗𝚊𝚖𝚎𝚍𝙶𝚛𝚘𝚞𝚙𝚜 system property, or per-connection:
SSLSocket tlsSock = (SSLSocket) SSLContext.getDefault() .getSocketFactory().createSocket();
SSLParameters params = tlsSock.getSSLParameters(); // Two hybrid KEM schemes plus two traditional ones
params.setNamedGroups(new String[] { "SecP256r1MLKEM768", "X25519MLKEM768", "secp256r1", "x25519" }); tlsSock.setSSLParameters(params);In June I flagged one caveat, and it is now closed. The underlying IETF specifications were still drafts, and the feature could only ship once they graduated to RFCs. Both did over the summer: the design document became RFC 9954 (Informational) in July, and the three mechanisms became RFC 10024, a Proposed Standard, in August. The JEP now points at both, and its paragraph about the risk of relying on drafts is gone.
Which means I can stop describing it and show it. This program asks Cloudflare’s post-quantum research endpoint which key exchange the connection used:
public class Kex {
public static void main(String[] args) throws Exception { var uri = URI.create("https://pq.cloudflareresearch.com/cdn-cgi/trace"); var body = HttpClient.newHttpClient() .send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()) .body(); body.lines().filter(l -> l.startsWith("kex=")) .forEach(l -> System.out.println("JDK " + Runtime.version().feature() + ": " + l)); } }The file is identical in both runs, and on JDK 27 the handshake comes back post-quantum. That is the entire point of the JEP.
JEP 538: PEM Encodings of Cryptographic Objects (Third Preview)
Sticking with the security library, JEP 538 takes the PEM encoding and decoding API through another round, after JEP 470 in JDK 25 and JEP 524 in JDK 26 (longtime readers will recall me covering both).
For the uninitiated: cryptographic objects live in binary DER format, great for machines, unreadable for humans. PEM (Privacy-Enhanced Mail) wraps that DER in Base64 with those familiar -----𝙱𝙴𝙶𝙸𝙽 ...----- headers, which is what actually ends up in your Git repos, server configs, and copy-pasted email threads. Until this API, Java had no first-class way to convert between the two, so everyone reached for hand-rolled parsers or third-party libraries.
String pem = PEMEncoder.of().encodeToString(privateKey); // object to PEM
PrivateKey key = PEMDecoder.of().decode(pem, PrivateKey.class); // PEM to objectIn June I described this one as the finalization. It is not: the “(Preview)” suffix stays for another release. The JEP was proposed to target JDK 27 as final on May 20, and a week later Mark Reinhold wrote on jdk-dev: “Due to late-breaking feedback, the owner of this JEP has decided do one more preview round.” The feedback itself never became public: it is not on security-dev, in the implementation PR or in the CSR. The owner is Anthony Scarpino.
The API changes I listed in June are in: the class for arbitrary PEM content is an ordinary class rather than a record, with constructors that take Base64 content as byte arrays, and 𝙳𝙴𝚁𝙴𝚗𝚌𝚘𝚍𝚊𝚋𝚕𝚎 is now 𝙱𝚒𝚗𝚊𝚛𝚢𝙴𝚗𝚌𝚘𝚍𝚊𝚋𝚕𝚎. A few I did not mention: a new 𝙲𝚛𝚢𝚙𝚝𝚘𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗, 𝙿𝙴𝙼𝙳𝚎𝚌𝚘𝚍𝚎𝚛.𝚠𝚒𝚝𝚑𝙵𝚊𝚌𝚝𝚘𝚛𝚢 renamed to 𝚠𝚒𝚝𝚑𝙵𝚊𝚌𝚝𝚘𝚛𝚒𝚎𝚜𝙾𝚏, 𝚐𝚎𝚝𝙺𝚎𝚢𝙿𝚊𝚒𝚛 methods on 𝙴𝚗𝚌𝚛𝚢𝚙𝚝𝚎𝚍𝙿𝚛𝚒𝚟𝚊𝚝𝚎𝙺𝚎𝚢𝙸𝚗𝚏𝚘, and 𝚐𝚎𝚝𝙺𝚎𝚢/𝚐𝚎𝚝𝙺𝚎𝚢𝙿𝚊𝚒𝚛 variants that take a 𝙺𝚎𝚢 instead of a password and a 𝙿𝚛𝚘𝚟𝚒𝚍𝚎𝚛.
The finish line is in place, though. JEP 542, the same title with no suffix, is integrated into JDK 28 and says: “We here propose to finalize the API without further change.” Code written against this preview should compile unchanged in March, minus --𝚎𝚗𝚊𝚋𝚕𝚎-𝚙𝚛𝚎𝚟𝚒𝚎𝚠.
Squeezing the Memory
JEP 534: Compact Object Headers by Default
JEP 534 promotes compact object headers to the default layout in HotSpot. The feature itself is the one we met in JDK 25 as a production option (JEP 519), having started life as an experimental flag back in JDK 24.
Quick refresher on why anyone cares: the standard 64-bit HotSpot object header is two machine words, a mark word and a class pointer. With millions of live objects, that second word adds up to tens of megabytes of pure overhead, doubles GC traffic, and trashes your CPU cache. Compact headers squeeze the mark word, a compressed class pointer, and a few meta bits into a single 64-bit value, while keeping all the functionality you rely on (hashCode, synchronization, locking).
After large-scale production hardening (Amazon famously ran it across hundreds of services), the layout has proven itself enough to become the default. From JDK 27 onward you get the memory savings for free, and if for some reason you need the old behaviour, -𝚇𝚇:-𝚄𝚜𝚎𝙲𝚘𝚖𝚙𝚊𝚌𝚝𝙾𝚋𝚓𝚎𝚌𝚝𝙷𝚎𝚊𝚍𝚎𝚛𝚜 is still there to turn it off. A rare case of a release where you do less and get more.
The most favourable case I could write in five lines: ten million records with two 𝚒𝚗𝚝 fields. Old layout, 12 bytes of header plus 8 of fields, rounded to 24. Compact, 8 plus 8, so 16. Plus 40 MB for the array of references in both runs:
Your own application will save less than the 29% here, because large arrays and strings gain almost nothing from a smaller header. Billy Korando, in his JDK 27 runtime notes, puts the typical figure at “about 20%” of heap. The release notes also say the off switch itself “is planned for deprecation and removal in a future release”, so enjoy it while it lasts. And keep it in mind when you get to section 4, because the one P1 bug of this release cycle only showed up with compact headers on.
JEP 523: Make G1 the Default Garbage Collector in All Environments
JEP 523 is the kind of change that sounds boring until you realize how many edge cases it quietly eliminates. As nipafx would gleefully remind us, the JCP doesn’t technically recognize the concept of a “default garbage collector”, but in practice HotSpot has to pick something when you don’t, and that pick has been inconsistent for a decade.
Here’s the history: G1 became the default for server-class machines in JDK 9 (JEP 248), that is, anything with at least 2 hardware threads and roughly 2 GB of RAM. Below that threshold (single CPU, less than ~1792 MB), HotSpot silently fell back to Serial GC, because back in 2017 Serial genuinely had the edge on throughput and footprint in cramped environments.
Note the past tense. Over the last several releases G1 has been relentlessly optimized: its native memory footprint came down, and the second card table introduced by JEP 522 in JDK 26 (the one I spent way too many paragraphs explaining in vol. 157) brought its throughput within spitting distance of Serial’s. G1’s maximum latencies, meanwhile, have always beaten Serial’s, since it reclaims the old generation incrementally rather than via stop-the-world full collections.
So the conclusion writes itself: G1 is now competitive at all heap sizes, and there’s no longer a good reason to surprise users with a different collector just because they’re running on a small box. From JDK 27, if you don’t specify a GC, you get G1, regardless of cores or memory. And of course, an explicit -𝚇𝚇:+𝚄𝚜𝚎𝚂𝚎𝚛𝚒𝚊𝚕𝙶𝙲 still does exactly what it says, this only changes the implicit choice. -𝚇𝚇:𝙰𝚌𝚝𝚒𝚟𝚎𝙿𝚛𝚘𝚌𝚎𝚜𝚜𝚘𝚛𝙲𝚘𝚞𝚗𝚝=𝟷 is enough to see it:
A smaller detail from June, where I put 𝙰𝚕𝚠𝚊𝚢𝚜𝙰𝚌𝚝𝙰𝚜𝚂𝚎𝚛𝚟𝚎𝚛𝙲𝚕𝚊𝚜𝚜𝙼𝚊𝚌𝚑𝚒𝚗𝚎 and 𝙽𝚎𝚟𝚎𝚛𝙰𝚌𝚝𝙰𝚜𝚂𝚎𝚛𝚟𝚎𝚛𝙲𝚕𝚊𝚜𝚜𝙼𝚊𝚌𝚑𝚒𝚗𝚎 down as “deprecated for removal”. They were deprecated one release earlier, in JDK 26 (JDK-8370843), and JDK 27 has already made them obsolete (JDK-8379665): the JVM prints “Ignoring option AlwaysActAsServerClassMachine; support was removed in 27.0” and starts anyway. If one of them sits in a Dockerfile from 2019, you get a line in the logs and nothing else.
Observability & Tooling
JEP 536: JFR In-Process Data Redaction
JEP 536 plugs a leak that’s bitten more teams than will admit it. JDK Flight Recorder captures, among other things, your command-line arguments, the initial values of environment variables, and system properties, which is fantastic for debugging and terrible when that .𝚓𝚏𝚛 file (often containing a database password or API token passed via -𝙳...) gets shipped off to a vendor, a support ticket, or a shared bucket.
This JEP lets JFR redact sensitive information in-process, before the recording is even finalized, so the secrets never make it to disk in the first place. It’s the sort of unglamorous, defense-in-depth feature that nobody asks for in a survey but everybody benefits from. In June it was still only Proposed to Target, and I wrote that it looked comfortable. It was.
The defaults are glob patterns on names (*𝚙𝚊𝚜𝚜𝚠𝚘𝚛𝚍*, 𝚜𝚎𝚌𝚛𝚎𝚝, 𝚝𝚘𝚔𝚎𝚗, 𝚊𝚙𝚒𝚔𝚎𝚢*, 𝚌𝚛𝚎𝚍𝚎𝚗𝚝𝚒𝚊𝚕 and a few more), and for command-line arguments they also swallow the next argument, so --𝚍𝚋𝚙𝚊𝚜𝚜𝚠𝚘𝚛𝚍 𝚇 loses both halves. The JEP’s own example, run on the GA build:
ACCESS_TOKEN=SECRET_TOKEN java -XX:StartFlightRecording:filename=dump.jfr -Xmx2G \ -Djavax.net.ssl.keyStorePassword=SECRET_PASSWORD \ App --dbpassword ANOTHER_SECRET_PASSWORD
-𝚇𝚖𝚡𝟸𝙶 stays readable, because nothing about it matches a pattern. That cuts both ways, which I found out while preparing this screenshot: my first run was in my normal shell, and the recording kept every environment variable whose name did not match a pattern, in plain text. The defaults only know names. If .𝚓𝚏𝚛 files leave your company, add your own patterns with -𝚇𝚇:𝙵𝚕𝚒𝚐𝚑𝚝𝚁𝚎𝚌𝚘𝚛𝚍𝚎𝚛𝙾𝚙𝚝𝚒𝚘𝚗𝚜:𝚛𝚎𝚍𝚊𝚌𝚝-𝚔𝚎𝚢=... (or load them from a file with @𝚔𝚎𝚢𝚜.𝚝𝚡𝚝) and check with -𝚇𝚕𝚘𝚐:𝚓𝚏𝚛+𝚛𝚎𝚍𝚊𝚌𝚝=𝚍𝚎𝚋𝚞𝚐. 𝚛𝚎𝚍𝚊𝚌𝚝-𝚔𝚎𝚢=𝚗𝚘𝚗𝚎,𝚛𝚎𝚍𝚊𝚌𝚝-𝚊𝚛𝚐𝚞𝚖𝚎𝚗𝚝=𝚗𝚘𝚗𝚎 turns the defaults off, and -𝚇𝚇:𝙵𝚕𝚒𝚐𝚑𝚝𝚁𝚎𝚌𝚘𝚛𝚍𝚎𝚛𝙾𝚙𝚝𝚒𝚘𝚗𝚜:𝚑𝚎𝚕𝚙 lists the options.
In June this section had a second entry, JEP 528. It is not in the release, and that is section 2.
Nihil Novi Sub Sole - (Yet More) Preview Features
And now the part of the release notes where, as ever, the preview features file past for another lap. If you’ve been reading this newsletter for any length of time, you can probably recite these from memory, but let’s note what (little) changed.
JEP 531: Lazy Constants (Third Preview)
The feature with more names than a witness in protection: Computed Constants, then StableValue (JEP 502, JDK 25), then LazyConstant (JEP 526, JDK 26), and now JEP 531 for round three.
The core idea has been stable throughout: compute a value exactly once, lazily, in a thread-safe way, and let the JVM treat it as a true constant afterwards (constant-folding and all), so you get lazy initialization without paying the usual synchronization tax.
The third preview makes two changes: it removes the low-level 𝚒𝚜𝙸𝚗𝚒𝚝𝚒𝚊𝚕𝚒𝚣𝚎𝚍() and 𝚘𝚛𝙴𝚕𝚜𝚎() methods (they invited usage patterns at odds with the API’s design goals), and it adds a 𝚂𝚎𝚝.𝚘𝚏𝙻𝚊𝚣𝚢(...) factory. With that addition, all three fundamental collection types, List, Set and Map, now have lazy variants where each element sits in its own LazyConstant and initializes only on first access.
As I showed back in vol. 171, this lets you write things like:
enum Option { VERBOSE, DRY_RUN, STRICT } // Each element initialized lazily, only on first touch
static final Set<Option> OPTIONS = Set.ofLazy(EnumSet.allOf(Option.class), Application::isEnabled);Thread-safe by construction, constant-foldable after the fact. I ran exactly this on the GA build with a 𝚙𝚛𝚒𝚗𝚝𝚕𝚗 inside 𝚒𝚜𝙴𝚗𝚊𝚋𝚕𝚎𝚍: the first 𝚌𝚘𝚗𝚝𝚊𝚒𝚗𝚜(𝚅𝙴𝚁𝙱𝙾𝚂𝙴) computed the value, the second did not, and 𝚂𝚃𝚁𝙸𝙲𝚃 waited until I asked about it.
In June I closed this entry by betting on finalization in JDK 28. In August a draft JEP for a fourth preview appeared, proposing “to re-preview the API with no changes in JDK 28”. It is only a draft, but the bet is not looking good.
JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)
JEP 532 brings back, for the fifth time (yes, fifth), the extension of pattern matching to primitive types across 𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎𝚘𝚏 and 𝚜𝚠𝚒𝚝𝚌𝚑. This round ships with no changes from the fourth preview (JEP 530, JDK 26), so, you know the drill by now.
switch (x.getYearlyFlights()) {
case 0 -> "No flights";
case int i when i >= 100 -> "Gold status!";
case int i -> "Regular: " + i + " flights";
}The authors are simply collecting more real-world feedback before pulling the trigger. One day this will be stable, and we’ll all pretend we weren’t slightly impatient about it.
JEP 533: Structured Concurrency (Seventh Preview)
JEP 533 continues the marathon, the seventh preview of Structured Concurrency, following JEP 525 (sixth preview) in JDK 26. The API treats a group of concurrent subtasks as a single unit of work, with unified cancellation and error propagation, and you still drive it through the 𝚂𝚝𝚛𝚞𝚌𝚝𝚞𝚛𝚎𝚍𝚃𝚊𝚜𝚔𝚂𝚌𝚘𝚙𝚎.𝚘𝚙𝚎𝚗() factory established in the big sixth-preview revamp.
In June I left it at that, with the same code sample I had used for JDK 26. This round deserves more room, because it is the biggest change to the API since the JDK 25 rework and it touches the signature of the method you call most. 𝚂𝚝𝚛𝚞𝚌𝚝𝚞𝚛𝚎𝚍𝚃𝚊𝚜𝚔𝚂𝚌𝚘𝚙𝚎 and 𝙹𝚘𝚒𝚗𝚎𝚛 now take a third type parameter for the exception that 𝚓𝚘𝚒𝚗() throws. The default policy and the 𝚊𝚕𝚕𝚂𝚞𝚌𝚌𝚎𝚜𝚜𝚏𝚞𝚕𝙾𝚛𝚃𝚑𝚛𝚘𝚠(), 𝚊𝚗𝚢𝚂𝚞𝚌𝚌𝚎𝚜𝚜𝚏𝚞𝚕𝙾𝚛𝚃𝚑𝚛𝚘𝚠() and 𝚊𝚠𝚊𝚒𝚝𝙰𝚕𝚕𝚂𝚞𝚌𝚌𝚎𝚜𝚜𝚏𝚞𝚕𝙾𝚛𝚃𝚑𝚛𝚘𝚠() joiners make 𝚓𝚘𝚒𝚗() throw a checked 𝙴𝚡𝚎𝚌𝚞𝚝𝚒𝚘𝚗𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗 with the failed subtask’s exception as the cause, and new overloads let you map it to something else. 𝚊𝚠𝚊𝚒𝚝𝙰𝚕𝚕() is gone, and 𝚘𝚗𝚃𝚒𝚖𝚎𝚘𝚞𝚝() is replaced by 𝚝𝚒𝚖𝚎𝚘𝚞𝚝(), which either produces the result or throws, with 𝙲𝚊𝚗𝚌𝚎𝚕𝚕𝚎𝚍𝙱𝚢𝚃𝚒𝚖𝚎𝚘𝚞𝚝𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗 as the cause.
Response handle() throws ExecutionException, InterruptedException { try (var scope = StructuredTaskScope.open()) { Subtask<String> user = scope.fork(() -> findUser()); Subtask<Integer> order = scope.fork(() -> fetchOrder()); scope.join(); // ExecutionException if any subtask failed return new Response(user.get(), order.get()); } }When I made 𝚏𝚎𝚝𝚌𝚑𝙾𝚛𝚍𝚎𝚛() throw an 𝙸𝚕𝚕𝚎𝚐𝚊𝚕𝚂𝚝𝚊𝚝𝚎𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗, the caller got an 𝙴𝚡𝚎𝚌𝚞𝚝𝚒𝚘𝚗𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗 with that exception as the cause. With a 50 ms timeout set through 𝚘𝚙𝚎𝚗(𝚓𝚘𝚒𝚗𝚎𝚛, 𝚌𝚏 -> 𝚌𝚏.𝚠𝚒𝚝𝚑𝚃𝚒𝚖𝚎𝚘𝚞𝚝(...)), the cause was 𝙲𝚊𝚗𝚌𝚎𝚕𝚕𝚎𝚍𝙱𝚢𝚃𝚒𝚖𝚎𝚘𝚞𝚝𝙴𝚡𝚌𝚎𝚙𝚝𝚒𝚘𝚗.
At seven previews, Structured Concurrency was officially testing my ability to find new ways to say “still cooking”. It may finally stop: JEP 543, “Structured Concurrency” with no suffix, became a Candidate on September 8 and proposes to finalize the API in JDK 28 “without further change”.
JEP 537: Vector API (Twelfth Incubator)
And to close, our perennial guest: JEP 537 delivers the twelfth incubation of the Vector API, with no substantial implementation changes since JDK 25, beyond the bundled SLEEF library for ARM and RISC-V vector math going from 3.6.1 to 3.9.0. The pitch is unchanged: express vector computations in plain Java that the JIT reliably compiles down to optimal SIMD instructions (AVX, NEON, SVE) on the host CPU.
As I’ve now written so many times it should be a macro: the Vector API stays in incubation until Project Valhalla‘s value classes become available as preview features, at which point it’ll be adapted to use them and promoted to preview. That condition is finally met somewhere: value objects are a preview feature in JDK 28 builds (more in the PS). No JEP says yet what that means for the Vector API itself.
2. The JEP that never boarded: why JEP 528 is not in JDK 27
In June I wrote that JEP 528 “had me a little nervous right up to the freeze - but it made the cut”. When those words went out on June 4, the JEP had been back in the Candidate state for fourteen days. Here is how that happened, reconstructed from the JEP’s change history in JBS, the jdk-dev archive and the implementation pull request.
What it was. Today jcmd only talks to a live JVM. When a JVM crashes you get an 𝚑𝚜_𝚎𝚛𝚛 file and, if you configured it, a core dump, and the tool that reads a HotSpot core dump is 𝚓𝚑𝚜𝚍𝚋, built on the Serviceability Agent. The JEP itself calls the SA codebase “brittle and dated”. JEP 528 proposed something bolder than a better parser: jcmd would bring the dead process back. It starts a helper process, maps the memory from the core file back at its original addresses, loads 𝚕𝚒𝚋𝚓𝚟𝚖 at its original address too, and then runs the same diagnostic command code a live JVM runs. David Holmes described the practical consequence in his review: the main thread of the new process impersonates the crashed JVM’s VM thread, and the VM pretends to be at a safepoint. According to the JEP, 26 of the 57 jcmd commands would work post mortem, on Linux core files and Windows minidumps, with macOS left as future work. The JBS issue, created by Kevin Walls on March 18, 2024, was originally titled “Process Reanimation for Serviceability”.
Two weeks in May. The JEP became a Candidate on October 16, 2025. On May 15, 2026, Walls moved it to Proposed to Target for JDK 27, and Reinhold moved it back to Candidate 74 minutes later, without a comment, at a point when the JEP had no endorsement yet. The next morning Mikael Vidstedt endorsed it and Walls proposed it again. On May 20 Reinhold sent the standard “JEP proposed to target JDK 27” mail, with objections due by May 26. On May 21 Walls moved the JEP back to Candidate himself and changed its fix version to 28. Nobody objected on jdk-dev. The only reply in the thread is Reinhold’s, five days later:
That sentence is everything on the record. The jdk-dev and serviceability-dev archives have the rest of the thread and nothing more, and JBS has no comment at all. What is documented is the state of the work on the day of the withdrawal.
The pull request on May 21. openjdk/jdk#31011, opened May 1: 61 files and about 6,800 added lines. Merging needed three reviews and an approved CSR. It had one review (the build part, from Erik Joelsson), two requests for changes (Leonid Mesnik on the tests, David Holmes on the VM code), and a CSR that Joe Darcy had moved to “Provisional, not Approved” on April 1.
The reviews read like a list of reasons why reviving a dead JVM is hard. Holmes, on May 8: “I can imagine that it is going to be very easy to break this because new code may need a revival check and do something special. I can also see these revival checks spreading across the code as you support more jcmds.” In the same review he calls “forcibly unlocking a Mutex that was in an unknown state” “very suspect”. On May 13 he rejected one fix with “It is UB to reinitialize an initialized mutex”, and accepted a workaround later that day after an offline discussion. Johan Sjölén asked how anyone is supposed to know where the 𝚃𝚑𝚛𝚎𝚊𝚍::𝚒𝚜_𝚛𝚎𝚟𝚒𝚟𝚎𝚍 checks belong. Yasumasa Suenaga asked whether it works on musl, meaning Alpine, and whether the new ELF parser could be shared with the ones HotSpot and the Serviceability Agent already have.
The discussion still open on the day of the withdrawal was about the public API. Reviving a core file needs a way to tell the Attach API, which has been in the JDK since version 6, “attach to this file”. Walls had added overloads, so that existing code attaching to PID 123 would not suddenly pick up a file named “123”. Alan Bateman, on May 19: “I think we should try to have the additions to the API to look like they were in the API from the start (when we added it in JDK 6). Right now the overloads are a bit problematic”. Walls’s last comment that day was “(Need to revert that.)”, a commit reverting the change followed on May 20, and on May 21 the JEP went back to Candidate.
The closest thing to an explanation came six weeks later, in the pull request:
Where it stands. The pull request is still open and was merged with master in August. On August 21 Walls answered Coleen Phillimore‘s review with “I hope to be updating this again soon!”. The JEP’s Release field has said “tbd” since June 10 and JDK 28’s list does not include it, although the implementation subtask and the CSR still carry fix version 28.
The practical lesson is a field on a web page. “Proposed to Target” is a proposal awaiting a decision, not the decision.
3. What the release notes say beyond the JEPs
The nine JEPs are only the headline, as with JDK 26. What follows comes from the OpenJDK release notes, the Oracle release notes and the CSR list, ordered by how much it will matter to a typical team.
JVMCI and the Graal JIT are gone from HotSpot
In vol. 176 I described the proposal to remove JVMCI, the interface that lets a compiler written in Java replace C2, and Amazon’s objection to it. Here is the ending. Paul Hohensee of Amazon opened the discussion on hotspot-compiler-dev on April 20, asking to delay the removal until Project Detroit “shakes out one way or the other”, and offering that “Amazon would be willing to take on JVMCI support if necessary”. Charles Nutter questioned whether keeping a “JVM Compiler Interface” is worth it mainly for languages that target the Graal JIT directly, and Volker Simonis replied that Truffle languages run on the same JVM, heap and GC as anything else. Tobias Hartmann then expanded the JBS issue with the costs, and on May 13 Vladimir Kozlov closed the discussion: “At this point, no compelling arguments have been raised that would justify keeping the JVMCI support in its existing form. We will therefore proceed to remove it.”
The removal (JDK-8382582, reported by Mikael Vidstedt, implemented by @Manuel Hässig) was integrated on May 26. The justification counts 252 #𝚒𝚏 𝙸𝙽𝙲𝙻𝚄𝙳𝙴_𝙹𝚅𝙼𝙲𝙸 blocks and estimates that about 1.5% of JDK contributions “unexpectedly need to take JVMCI into account”. The CSR sums it up in one line: “This is an experiment that has outlived its usefulness.”
In practice: from JDK 27 you cannot run the Graal JIT, or Truffle languages with Graal compilation, on a stock OpenJDK build. The CSR says projects that need JVMCI “should carry and maintain it in their own downstream trees”. GraalVM itself has no JDK 27 release anyway: as I described in vol. 185, it ships monthly releases on a JDK 25 base and its next stop is Java 29.
HttpServer now matches path segments
The built-in 𝚌𝚘𝚖.𝚜𝚞𝚗.𝚗𝚎𝚝.𝚑𝚝𝚝𝚙𝚜𝚎𝚛𝚟𝚎𝚛.𝙷𝚝𝚝𝚙𝚂𝚎𝚛𝚟𝚎𝚛 used to match a request against a context with a plain string prefix, so a context registered as /𝚏𝚘𝚘 also received /𝚏𝚘𝚘𝚋𝚊𝚛. From JDK 27 it matches whole path segments (JDK-8272758):
It is a fix and a behaviour change at once, and plenty of test harnesses and small internal tools run on this server. The 𝚜𝚞𝚗.𝚗𝚎𝚝.𝚑𝚝𝚝𝚙𝚜𝚎𝚛𝚟𝚎𝚛.𝚙𝚊𝚝𝚑𝙼𝚊𝚝𝚌𝚑𝚎𝚛 system property brings the old matching back, and the release notes say it may be removed later.
A few small APIs
𝙼𝚊𝚝𝚑 and 𝚂𝚝𝚛𝚒𝚌𝚝𝙼𝚊𝚝𝚑 gain 𝚊𝚜𝚒𝚗𝚑, 𝚊𝚌𝚘𝚜𝚑 and 𝚊𝚝𝚊𝚗𝚑, ports of the fdlibm versions. 𝚂𝚝𝚛𝚒𝚗𝚐.𝚎𝚗𝚌𝚘𝚍𝚎𝚍𝙻𝚎𝚗𝚐𝚝𝚑(𝙲𝚑𝚊𝚛𝚜𝚎𝚝) returns the byte length of the encoded string without allocating the array that 𝚐𝚎𝚝𝙱𝚢𝚝𝚎𝚜(𝚌𝚜).𝚕𝚎𝚗𝚐𝚝𝚑 creates (JDK-8375318). On my test string “zażółć” it returns 10, against a 𝚕𝚎𝚗𝚐𝚝𝚑() of 6. And 𝙱𝚒𝚐𝙳𝚎𝚌𝚒𝚖𝚊𝚕.𝚛𝚘𝚘𝚝𝚗(𝚒𝚗𝚝, 𝙼𝚊𝚝𝚑𝙲𝚘𝚗𝚝𝚎𝚡𝚝), contributed by Fabio Romano (JDK-8366479), completes 𝚙𝚘𝚠 and 𝚜𝚚𝚛𝚝. I checked it in jshell with the obvious input: 𝚗𝚎𝚠 𝙱𝚒𝚐𝙳𝚎𝚌𝚒𝚖𝚊𝚕(”𝟸𝟽”).𝚛𝚘𝚘𝚝𝚗(𝟹, 𝙼𝚊𝚝𝚑𝙲𝚘𝚗𝚝𝚎𝚡𝚝.𝙳𝙴𝙲𝙸𝙼𝙰𝙻𝟼𝟺) returns 𝟹.
Consolation prizes for jcmd
While JEP 528 waits, jcmd got smaller gifts. On Linux, 𝚜𝚘𝚞𝚛𝚌𝚎 $𝙹𝙰𝚅𝙰_𝙷𝙾𝙼𝙴/𝚌𝚘𝚗𝚏/𝚋𝚊𝚜𝚑-𝚌𝚘𝚖𝚙𝚕𝚎𝚝𝚒𝚘𝚗/𝚓𝚌𝚖𝚍 gives you tab completion. A new 𝚅𝙼.𝚜𝚎𝚌𝚞𝚛𝚒𝚝𝚢_𝚙𝚛𝚘𝚙𝚎𝚛𝚝𝚒𝚎𝚜 command prints the security properties of a running JVM (JDK-8364182). And 𝚅𝙼.𝚒𝚗𝚏𝚘 and 𝚑𝚜_𝚎𝚛𝚛 files now report the number of open file descriptors (JDK-8359706), which is the first thing I would check after a “Too many open files” crash anyway.
The TLS defaults move again, beyond JEP 527
Two more changes to the default TLS configuration travel with the post-quantum work. ffdhe6144 and ffdhe8192 were dropped from the default list of named groups, because they are “almost never used in practice” and cost host resources to process (JDK-8373426). And X25519 itself got faster, by 55% for key generation and 51% for key agreement (JDK-8378893), which pays for part of the heavier hybrid handshake you now negotiate by default.
Two more security changes to know before upgrading
TLS 1.3 certificate compression with zlib (RFC 8879) is on by default (JDK-8372526), which makes handshakes with long certificate chains smaller. And ML-KEM and ML-DSA private keys are now encoded in PKCS#8 using the 𝚜𝚎𝚎𝚍 format by default (JDK-8347938). The release note says it plainly: keys generated by this release “will not be accepted by older releases by default”. If you generate post-quantum keys on JDK 27 and read them on JDK 25, set 𝚓𝚍𝚔.𝚖𝚕𝚔𝚎𝚖.𝚙𝚔𝚌𝚜𝟾.𝚎𝚗𝚌𝚘𝚍𝚒𝚗𝚐 or 𝚓𝚍𝚔.𝚖𝚕𝚍𝚜𝚊.𝚙𝚔𝚌𝚜𝟾.𝚎𝚗𝚌𝚘𝚍𝚒𝚗𝚐 back to 𝚎𝚡𝚙𝚊𝚗𝚍𝚎𝚍𝙺𝚎𝚢.
Finalization loses one more method
𝚃𝚑𝚛𝚎𝚊𝚍𝙿𝚘𝚘𝚕𝙴𝚡𝚎𝚌𝚞𝚝𝚘𝚛.𝚏𝚒𝚗𝚊𝚕𝚒𝚣𝚎() is removed (JDK-8371748). It was deprecated in JDK 9, re-specified in JDK 11 to do nothing, and deprecated for removal in JDK 18. If you extend 𝚃𝚑𝚛𝚎𝚊𝚍𝙿𝚘𝚘𝚕𝙴𝚡𝚎𝚌𝚞𝚝𝚘𝚛 and your 𝚏𝚒𝚗𝚊𝚕𝚒𝚣𝚎() calls 𝚜𝚞𝚙𝚎𝚛.𝚏𝚒𝚗𝚊𝚕𝚒𝚣𝚎(), that now resolves to 𝙾𝚋𝚓𝚎𝚌𝚝.𝚏𝚒𝚗𝚊𝚕𝚒𝚣𝚎(), which declares 𝚝𝚑𝚛𝚘𝚠𝚜 𝚃𝚑𝚛𝚘𝚠𝚊𝚋𝚕𝚎, and your code may stop compiling.
No Intel Mac builds from Oracle and Adoptium
jdk.java.net/27 offers four builds: Linux x64 and AArch64, macOS AArch64 and Windows x64. There is no macOS x64. Adoptium announced the same for Temurin on September 2, in a post by George Adams that cites “hardware availability and the retirement of the upstream port”. The formal step is JEP 541, which deprecates the port for removal and is targeted to JDK 28. Other vendors decide for themselves: Azul already ships a Zulu 27 for Intel Macs.
4. What happened around the release
The release candidate that came two weeks late, and the bug that made the point
In vol. 188 I wrote about the move to monthly security updates and how it pushed the first JDK 27 release candidate from August 6 to August 20. In his August 6 mail, Reinhold explained why a feedback window shorter by two weeks was acceptable: since JDK 10, fewer than half of the releases have needed a second RC, “and none of the bugs that triggered those builds was reported by an end user. This suggests that the risk is tolerable.”
Twelve days later, Chris Hegarty filed JDK-8390546 as a P1, on behalf of Lorenzo Dematté. His team had started running its integration tests on JDK 27 early-access builds, and a string that the tests serialized and read back came out with the first four bytes wrong. The report narrows it down to C2 compiling a method that inlines 𝙰𝚛𝚛𝚊𝚢𝚜.𝚌𝚘𝚙𝚢𝙾𝚏𝚁𝚊𝚗𝚐𝚎, on x64 Linux (it did not reproduce on ARM), and it goes away with -𝚇𝚇:-𝚄𝚜𝚎𝙲𝚘𝚖𝚙𝚊𝚌𝚝𝙾𝚋𝚓𝚎𝚌𝚝𝙷𝚎𝚊𝚍𝚎𝚛𝚜. Tobias Hartmann traced it to a change in build 24 that gave an intrinsic the wrong memory alias, Reinhold replied “P1 was the correct priority”, and the offending change was backed out on August 19 (JDK-8390590), a day before the RC.
So strictly speaking the RC was not affected. But under the original schedule build 34 would have been the first release candidate, and it contained this bug, so it would have forced a second RC, triggered by exactly the kind of report that the August 6 mail said had never caused one. (Reinhold’s GA mail, fittingly, calls build 35 “the second Release Candidate”, while his August 20 mail called it the first.) If you want to help the next release, run your test suite on the early-access builds. This bug was found exactly that way.
Oracle’s announcement
Oracle’s press release quotes Georges Saab, and one sentence in that quote is for anyone paying for Oracle JDK: Oracle is “executing on a roadmap to bring comparable capabilities to JDK releases with long-term support offered by Oracle”. That means post-quantum TLS in the LTS releases, with no versions or dates given.
The release came with Helidon 27, whose version numbers and cadence now follow OpenJDK. It adds Scoped Values, Helidon Messaging with Kafka and JMS connectors, and Helidon Data JDBC, although on release day Maven Central only had 𝟸𝟽.𝟶.𝟶-𝙼𝟷. JavaFX 27 gets a Metal rendering pipeline on macOS. And Oracle Jipher 20, a wrapper around a FIPS 140-3 validated OpenSSL module that now supports ML-KEM and ML-DSA, joins the Java Verified Portfolio I described in the JavaOne edition.
The technical post on Inside Java, The Arrival of Java 27, notes that this is the 18th feature release delivered on time under the six-month cadence, and that smaller organizations and independent developers “collectively contributed 16% of the fixes”.
Builds and tools on day one
SapMachine 27 was on GitHub at 08:54 UTC on September 15, before Oracle’s announcement. Azul Zulu 27 and Amazon Corretto 27.0.0.35.1 are out. As I write this on Wednesday, Temurin 27 is not out yet. Microsoft builds only LTS releases, so there will be no Microsoft Build of OpenJDK 27.
IntelliJ IDEA supports Java 27 from day one, as 👩🏻💻 Marit van Dijk describes in Java 27 in IntelliJ IDEA. Gradle 9.8.0-RC1 supports Java 27 “for both the Gradle daemon and Java toolchains”, with the final release still to come. Kotlin gets the JVM 27 bytecode target in 2.5.0-Beta1 (KT-87767).
Go and read this one
On the day of the release Johannes Bechberger published his own reckoning with JDK 27, Java 27 is only boring on the surface. He picks a different set of changes than I did and argues for them from the JVM side, which is where he spends his working days.
I am deliberately not summarising it, because the pleasure of that text is in the order he puts things in. Set aside a coffee break for it.
PS: Valhalla is in JDK 28. JEP 401, now titled “Value Objects (Preview)”, is integrated into the mainline and marked for release 28. Dan Smith wrote on valhalla-dev on August 4 that the 𝚕𝚠𝚘𝚛𝚕𝚍 branch is closed and that his mail client had collected “over 6800 messages in the 215 days since Jan 1”, and on August 10 he asked people to test the 28 early-access builds both with and without --𝚎𝚗𝚊𝚋𝚕𝚎-𝚙𝚛𝚎𝚟𝚒𝚎𝚠, because the integration “perturbs a very large amount of code”.
The first reports are coming in: Ivan Ponomarev measured 𝚂𝚢𝚜𝚝𝚎𝚖.𝚊𝚛𝚛𝚊𝚢𝚌𝚘𝚙𝚢 as 8 to 20 times slower than a plain loop on null-restricted flat value arrays. In vol. 171 I bet on JEP 401 as the most important JEP of JDK 27, so that is the second bet in this edition that did not age well 🥶 JDK 28 so far: 401, 535 (generational Shenandoah by default), 539, 540 (a simple JSON API, incubator), 541 and 542, plus two new candidates from September, 543 (Structured Concurrency, final) and 544 (Ahead-of-Time Code Compilation, from Leyden, owned by John Rose).
That will be a longer edition in December.
PS2: Closing a thread from vol. 186 and vol. 189: JDK-8377715, where thawing a virtual thread’s frame could undo a deoptimization, was fixed in the JDK 27 mainline in April, so JDK 27 ships with the fix.
PS3: And since he shows up above and I know he reads this: congratulations to Johannes Bechberger on becoming a Java Champion. Anyone who has spent a week inside a profiler knows how much of that work is thankless. Very well deserved, Johannes.
PS4: And Jarek Dąbrowski and JetBrains made my day: KotlinConf 2027 comes to Kraków - my hometown - April 21 to 23, at the ICE Kraków Congress Centre. The conference has been running since 2017 and has never been held in Poland, so after San Francisco, Amsterdam, Copenhagen and Munich it is finally our turn. I cannot wait.

























