BLOG
Tech Teardown 010 | Java 27: Three New Features Dissected Down to the Source Code — 64-bit Object Headers, Primitive Type Pattern Matching, Structured Concurrency
On September 15, 2026, JDK 27 was officially released (GA, build 35), bringing 9 JEPs. Most coverage stops at the press-release level: a number, a one-line summary, a sample snippet. This article takes a different approach — going straight to the source code. Three features in this release each sit at a different layer: JEP 534 Compact Object Headers (the HotSpot runtime) compresses the object header on 64-bit architectures from 96 bits down to 64 bits; JEP 532 Primitive Types in Patterns (javac) makes instanceof and switch work with all primitive types; JEP 533 Structured Concurrency (the java.base API) gathers a group of related thread tasks into a closeable scope. This article breaks down six things: what it is, the core mechanics traced down to source line numbers, a technical assessment, whether it’s worth adopting, how to enable it, and the path from reading source code to submitting patches to OpenJDK.
1. What Is This
Let’s lay out the full picture first. JDK 27 contains 9 JEPs in total, verified against the official project page: JEP 523 G1 default across all environments, JEP 527 TLS 1.3 Post-Quantum Hybrid Key Exchange, JEP 531 Lazy Constants (3rd preview), JEP 532 Primitive Type Pattern Matching (5th preview, a focus of this article), JEP 533 Structured Concurrency (7th preview, a focus of this article), JEP 534 Compact Object Headers enabled by default (final feature, a focus of this article), JEP 536 JFR In-Process Data Masking, JEP 537 Vector API (12th incubation), JEP 538 PEM Encoding (3rd preview). Note that the numbers are not consecutive — 524–530 and 535 are missing.
The three focus features each govern a different layer, and their maturity levels differ widely. JEP 534 is a final feature, enabled by default in JDK 27; it changes HotSpot’s object memory layout and is completely transparent to Java code. History: introduced experimentally as JEP 450 (JDK 24), finalized but not default as JEP 519 (JDK 25), and enabled by default as JEP 534 (JDK 27); the Owner is Roman Kennke. JEP 532 is a preview feature at the language level, requiring --enable-preview; it addresses an awkwardness that has been accumulating for over twenty years: pattern matching, instanceof, and switch have long only recognized reference types. JEP 533 is a preview feature at the API level, likewise requiring --enable-preview; it takes “a group of related tasks” out of the free-for-all state of thread pools and turns it into work units with clear boundaries. The Authors are Alan Bateman, Viktor Klang, and Ron Pressler, carrying it forward from JEP 428 (incubated in JDK 19) all the way to today.
A single thread ties all three together: saving memory, lightening the compiler’s load, making concurrency easier to manage — it’s all engineering pragmatism. No new paradigms, just paying off old debts.
2. Core Mechanics
First, a note on the citation rules: the “JEP account” comes from the official JEP pages on openjdk.org; the “source-code view” is the verbatim source text in the openjdk/jdk repository at tag jdk-27+35, complete with file paths and line numbers.
2.1 JEP 534: 96 bits squeezed into 64 bits — the bit layout in markWord.hpp is the whole answer
On 64-bit architectures, the old object header consists of a mark word (64 bit) plus a class word (32 bit when compressed class pointers are enabled), 96 bits in total. JEP 450’s idea is to drop the boundary between the two segments and stuff the compressed class pointer into the mark word. The official layout diagram (verbatim from JEP 450):
Header (compact):
64 42 11 7 3 0
[CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT]
(Compressed Class Pointer) (Hash Code) /(GC Age)^(Tag)
(Valhalla-reserved bits)(Self Forwarded Tag)
The class pointer is squeezed further down to 22 bits, the hash code keeps its size, and 4 bits are reserved for Project Valhalla. That’s the JEP account; the source code below lines up with it bit by bit. Here is the bit-layout diagram from the header comment in markWord.hpp L43–49:
// 64 bits (without compact headers):
// unused:22 hash:31 valhalla:4 age:4 self-fwd:1 lock:2
// 64 bits (with compact headers):
// klass:22 hash:31 valhalla:4 age:4 self-fwd:1 lock:2
In one sentence: the top 22 bits, previously unused, now hold klass; the other five bit fields remain completely untouched, and everything adds up to exactly 64 bits. The constant definitions (same file, L116–154, with the key lines excerpted):
static const int lock_bits = 2;
static const int self_fwd_bits = 1;
static const int age_bits = 4;
static const int hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits;
// Used only with compact headers: the (narrow) Klass* lives in bits 43 to 64.
static constexpr int klass_bits = 22;
Three details: hash is explicitly capped at 31 bits, consistent with JEP 450’s “hash code keeps its size”; the 22-bit narrow Klass lives in bits 43–64; and the bit fields run from low to high as lock(2)→self-fwd(1)→age(4)→valhalla reserved(4)→hash(31)→klass(22) — the comment, the constants, and the sum are all mutually consistent. On the switch side (globals.hpp L131–132):
product(bool, UseCompactObjectHeaders, true,
"Use compact 64-bit object headers in 64-bit VM")
A full product switch on LP64 platforms, defaulting to true. For historical comparison: in the JEP 450 era it was an experimental switch requiring -XX:+UnlockExperimentalVMOptions; in JDK 27 it is just an ordinary product flag line. Line 150 of the same file states that it is always false on 32-bit VMs. To switch back to the old layout, use -XX:-UseCompactObjectHeaders; the old 96-bit layout is still retained in this release, and JEP 534 explicitly lists removing the old layout as a non-goal. Scope boundary: mechanism descriptions such as lock operations no longer overwriting the mark word, and GC forwarding adding a self-forwarded tag, come from the JEP 450 documentation; self_fwd_bits is corroborated at the comment level; the concrete C++ code for ObjectMonitor and GC forwarding is outside the scope of this article and will not be covered.
2.2 JEP 532: Primitive Type Pattern Matching — What javac Does During Lowering
Three historical limitations (per the JEP): pattern matching for switch does not support primitive type patterns; primitive components of record patterns must be strictly of the same type (JsonNumber(double a) cannot be written as JsonNumber(int age)), even though the rest of the language has automatic widening; instanceof only supports reference types. JEP 532 opens all of these up at once, extending the switch selector to long/float/double/boolean. The semantic core is exactness — a conversion is exact if no information is lost; whether long→int or int→float is exact depends on the runtime input value and requires a runtime test; unconditionally exact, by contrast, can be determined at compile time to never lose information (in two forms, type-based and value-based). The dominance and exhaustiveness rules are extended accordingly, and floating-point case constants are deduplicated by representational equivalence. Official example (verbatim from JEP 532):
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i; // 原 default 分支
}
int i = 1000;
if (i instanceof byte b) { ... } // false,不进入分支
float f = 1000.0f;
f instanceof int; // true (exact)
What we see in the source (TransPatterns.java). The instanceof primitive type test is rewritten during the lowering phase, L200–221:
// $expr instanceof $primitiveType
// =>
// $expr instanceof T $temp && $temp instanceof $primitiveType
if (tree.erasedExprOriginalType!=null && ...) {
BindingSymbol temp = new BindingSymbol(Flags.FINAL | Flags.SYNTHETIC, ...);
// 先对擦除前类型做绑定模式匹配,再对临时变量做原始类型测试
result = translate(resultExpr); // 两段式 && 复合表达式
}
A single expr instanceof int does not directly generate primitive type-test bytecode; instead it is a two-stage AND operation: first the value is captured into a synthetic FINAL | SYNTHETIC temporary variable (its name contains syntheticNameChar, so it never collides with user variables), and then it asks, “did this conversion lose any information?” The first stage is responsible for safely bringing the value back; the second is the runtime exactness check. makePrimitive at L772–828 gives the other half of the answer: primitive types obtain their Class objects via the ConstantBootstraps.primitiveClass constant bootstrap method (condy), and the signatures are assembled by PrimitiveGenerator — the compiler is generating the invokedynamic and constant-pool material for primitive type patterns. In addition, at L510 the null check is waived when the selector is a primitive type (primitive types have no null); the binding pattern’s null check at L935 splits into two branches depending on whether the type is primitive. Scope boundary: the complete rules for exactness, dominance, and exhaustiveness are taken solely from the JEP documentation; the compile-time implementations in Attr.java and Check.java are outside the scope of this article.
2.3 JEP 533: Structured Concurrency — a sealed interface plus a three-state state machine
Structured concurrency treats a group of related tasks as a single unit of work: subtasks run on virtual threads by default; fork/join/close may only be called by the owner thread, and violations throw StructureViolationException; a failing subtask short-circuits and cancels the rest; if the owner is interrupted, the scope is closed and all subtasks are cancelled; subtasks inherit ScopedValues; the JSON thread dump displays the task hierarchy tree (all of the above runtime behavior follows the JEP’s account). Five changes in this version (JEP 533 History): the interface and Joiner gain a third type parameter R_X (the exception type thrown by join()); a new open(UnaryOperator) is added; three factories such as allSuccessfulOrThrow() make join() throw ExecutionException, and each gains an overload taking a Function; Joiner.awaitAll() is removed; onTimeout() is replaced by timeout(), with the timeout exception carrying CancelledByTimeoutException as its cause. Official example (from the JEP 533 text):
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join();
return new Response(user.get(), order.get());
}
What the source shows (StructuredTaskScope.java, 1469 lines in the full file, implementation class StructuredTaskScopeImpl). L373–421:
public sealed interface StructuredTaskScope<T, R, R_X extends Throwable>
extends AutoCloseable
permits StructuredTaskScopeImpl {
sealed interface Subtask<T> extends Supplier<T> permits StructuredTaskScopeImpl.SubtaskImpl {
enum State { UNAVAILABLE, SUCCESS, FAILED }
Two sealed declarations pin down the structure: the scope permits only the package-private implementation class, and Subtask permits only SubtaskImpl. During incubation this API lived in jdk.incubator.concurrent; its official home in JDK 27 is java.util.concurrent inside java.base—from incubator graduation to preview, the module placement is an evolution marker. The Subtask state machine has only three states, and extends Supplier<T> means get() simply retrieves the result, without all of Future’s assorted baggage. The Joiner default methods (L572–618) write the state discipline into code: onFork requires the subtask to be UNAVAILABLE, and onComplete requires that it is no longer UNAVAILABLE—two assertions that strictly bound what the callbacks can observe. The open factory’s layered defaults (L1268–1269): the zero-argument open() goes through Joiner.awaitAllSuccessfulOrThrow()—any single subtask failure fails the whole thing—and the javadoc (L1173–1176) states that the default configuration creates unnamed virtual threads with no timeout. Every public API in the file carries @PreviewFeature(STRUCTURED_CONCURRENCY)—confirming once again, at the source level, that this is still a preview API. Boundary of this analysis: the virtual thread creation point, the interrupt calls for cancellation propagation, and the timeout scheduling mechanism all live in the implementation class; this article does not quote its internal code.
III. Technical Assessment
First, let’s be clear about the form of the evidence: all source-code citations come from the jdk-27+35 tag; all performance figures are quoted from the official JEP pages — official figures, not independent re-benchmarks.
JEP 534: The mechanism is provable; the benefits rest on official word. At the mechanism level, three pieces of source code — the bit-layout comments, the bit-field constants, and the default switch — interlock; the compression scheme is fully self-consistent in the code, which is hard evidence. At the benefits level, the officially cited figures: in one scenario, SPECjbb2015 used 22% less heap and 8% less CPU time; in another scenario, GC counts dropped by 15% (true for both G1 and Parallel); a highly parallel JSON parsing benchmark ran 10% faster. The endorsements likewise come from the JEP’s own text: hundreds of Amazon production services are using it (most backported to JDK 21/17), and SAP’s SapMachine has it enabled by default. The risk is spelled out plainly: 4 bits are reserved for Valhalla, and if that proves insufficient, the class pointer and identity hash code can be squeezed further (per JEP 450). A dose of cold water: klass_bits is hard-coded at 22, further tightening the addressing ceiling of class space — a deliberate trade-off of bit width for default-on enablement.
JEP 532: Semantics stable, still a preview. This is the 5th preview, and this version goes back to preview with no changes relative to JDK 26 (JEP 530) — the signal being that the semantics have essentially converged; but the “5th preview” itself means it hasn’t been finalized: the syntax still has room to shift, and a large-scale production rollout carries rework risk. At the implementation level, two-stage lowering plus fetching the Class via condy shows this is not lightweight inside the compiler — bringing primitive types into pattern matching comes at the cost of javac generating an extra intermediate layer.
JEP 533: The API surface is converging. The parts that changed most frequently across the seven previews — the construction approach, the completion policies, the timeout mechanism — have by JDK 27 settled into three moves: R_X plus open(UnaryOperator) plus timeout(), the typical shape of a convergence phase. Sealed interfaces constrain the implementations, and the state machine has been cut down to three states — the design restraint is visible. Cold water: a 7th preview means no commitment to permanent compatibility, and the internal mechanics of cancellation propagation and timeouts live in the implementation classes, so evaluation can only stop at the level of interface semantics.
Taken together. The maturity ladder is clear: with 534, you get it simply by upgrading; 532 and 533 require flipping preview flags, and neither belongs in critical paths that demand long-term stability. None of the three introduces new concepts — compact object headers are bit reuse, primitive type pattern matching folds widening into patterns, and structured concurrency carries the structured principle into concurrency — the other face of engineering pragmatism: no surprises, and no magic.
4. Value Judgment
The problems are real, and genuinely so: object header overhead has long been a criticized sore point on 64-bit heaps; instanceof and switch rejecting primitive types is an old wound in language consistency; and error handling and cancellation propagation in concurrent code is among the areas where Java programmers have the highest incident rates. Each of the three JEPs targets one real problem.
AI is writing more and more of the code, yet the value of humans reading source is actually rising — one light remark: once a large share of code is generated by models, where should the time people save go? One highly cost-effective destination is to read one layer deeper: see what bytecode the compiler turns the new syntax into, and which bits of the object header the runtime touches. The compiler and the runtime don’t get simpler just because you haven’t read them; the more language features there are, the wider the gap between “what actually happens in the source” and “the code you wrote”. expr instanceof int being rewritten into a two-stage form of a binding pattern plus an AND operation is a case in point: glancing only at the syntactic sugar, you’d think it’s a single test; only after reading TransPatterns do you learn it’s a type reclamation plus a runtime exactness check.
Saving memory has direct significance for AI service deployment, but don’t overstate it: for inference services, retrieval services, and Agent gateways on the JVM, every notch the heap footprint drops is a notch shaved off the cloud bill, and cutting the object header by a third is a clearly favorable direction for services with high object density; but a “direction” doesn’t mean “your service is exactly that 22%” — the actual gain depends on your object size distribution and allocation rate.
Draw the boundaries clearly: 532 and 533 are previews, and JDK 28 may adjust them further or preview them again — wait until they’re finalized before using them in production code. 534 is enabled by default but can be rolled back; if compatibility problems arise with monitoring tools or native agents, the rollback switch is still there. The performance figures and endorsements in this article all come from official statements — keep to the same framing when citing them. When it’s worth chasing: if you maintain JVM services, care about the memory bill, or write concurrency-heavy code, turn on --enable-preview now and get familiar with 532 and 533. When it’s not worth chasing: critical production paths that require permanently stable APIs — wait until they’re finalized.
5. How to Put It into Practice
The flag. JDK 27 is already GA (2026-09-15, build 35). JEP 534 is on by default with no flags needed — active on LP64, permanently off on 32-bit; verify with -XX:+PrintFlagsFinal that UseCompactObjectHeaders is true. JEP 532 and 533 are previews: both the compile and run sides need --enable-preview (javac additionally takes --release 27); missing either side means outright rejection.
Run the samples. The official examples in sections 2.2 and 2.3 are the minimal entry point. Two variants are worth trying: swap case int i for case long i to see the dominance error; replace Joiner.anySuccessfulOrThrow() with the default open() to see the difference in failure propagation. Trying it once with your own hands beats reading the rules ten times.
Read the source. Two routes: the installed JDK ships with lib/src.zip — unzip it and you have the java.base and jdk.compiler sources; for HotSpot’s C++ go through GitHub openjdk/jdk and checkout tag jdk-27+35. Four files to go straight to: src/hotspot/share/oops/markWord.hpp (see the comments at L40–76 and the constants at L116–154), src/hotspot/share/runtime/globals.hpp (the flag at L131), src/jdk.compiler/share/classes/com/sun/tools/javac/comp/TransPatterns.java (L200–221, L772–828), src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java (L373–1468). How to read them: start with the header comments and javadoc — OpenJDK’s comment density is extremely high, with bit-layout diagrams and transformation rules all written into the comments — then jump into the implementation with specific questions in mind; don’t grind through it line by line.
6. How to Build a Similar Solution Yourself
For this column, the “reproduce it yourself” approach applied to the JDK — the most realistic path is to move from reading the source code to submitting a small patch to OpenJDK. OpenJDK is a massive engineering effort, but it is not a high wall for individual contributors. The path falls into four stages.
Stage one: build a foundation by reading the source code. Start with the four files from Section 5; once you’ve digested the header comments, follow the trail: from markWord.hpp read down into the oops hierarchy, from TransPatterns.java read up into TreeTranslator and its neighbors in the same directory, Attr and Check, and StructuredTaskScope.java connects straight to its implementation classes. The goal is not to understand everything, but to build an intuition for “if I change this file, who else is affected.”
Stage two: build locally. openjdk/jdk ships with its own build system; on Unix-like platforms, run ./configure followed by make images. The first full build takes one to two hours; incremental builds after that take tens of seconds. Being able to build locally is what earns you your lab bench.
Stage three: run tests, then change code. OpenJDK uses jtreg: javac tests live in test/langtools, HotSpot tests in test/hotspot/jtreg, and JDK API tests in test/jdk. The right approach is to first write a new failing test case (edge-case tests for preview features are especially welcome), then modify the code until it passes.
Stage four: the submission process. The standard workflow: style and commit-format checks use the repository’s built-in jcheck; changes are posted as a webrev to the component’s review list and reviewed by a Committer; before your first contribution you sign the OCA (Oracle Contributor Agreement), done electronically, with the official contributors page as the authoritative reference. Where to land: 532 and 533 are in their preview period, where low-risk improvements such as javadoc wording, error messages, and edge-case tests are genuine and welcome entry points; HotSpot’s bit-layout and flag documentation likewise accepts small fixes year-round. For exact ownership and mentoring arrangements, defer to the current instructions on each project’s page.
Compressed into a single diagram:
读源码(markWord.hpp / TransPatterns.java / StructuredTaskScope.java)
→ ./configure && make images(本地构建 JDK 27 镜像)
→ jtreg 跑目标组件测试(test/langtools、test/jdk……)
→ 先写失败用例,再改实现,再跑绿
→ jcheck → webrev 挂评审 → OCA(首次)→ Committer 合并
An honest difficulty distribution: a proficient developer can complete the first two stages within two weeks; from the third stage on, your understanding of the component is tested; and the review back-and-forth in the fourth stage takes the longest. But the payoff is one you won’t find anywhere else: every line you change will run next year on hundreds of millions of JVMs worldwide.
Conclusion
The three JDK 27 features are three sides of the same coin: JEP 534 shrinks the object header from 96 bits down to 64 bits — the bit layout in markWord.hpp and the default flags in globals.hpp prove it’s mature engineering, with the gains to be measured per the official figures; JEP 532 makes instanceof and switch support all primitive types, with TransPatterns’ two-stage lowering building out the runtime exactness checks, and a 5th re-preview with no changes meaning the semantics have converged; JEP 533 folds structured concurrency into java.base, with the sealed interface, the three-state state machine, and the awaitAllSuccessfulOrThrow default policy all plainly visible in the source code. None of the three is a new paradigm — they’re all old debts being paid off. You get 534 the moment you upgrade; for the other two, turn on --enable-preview to get familiar with them, and wait until they’re finalized before putting them into production.
References
- JEP 534: Compact Object Headers by Default (openjdk.org/jeps/534; Owner: Roman Kennke; Closed/Delivered, Release 27)
- JEP 450: Compact Object Headers (Experimental) (openjdk.org/jeps/450; details on the compact layout bitmap and compression mechanism)
- JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview) (openjdk.org/jeps/532; Owner: Angelos Bimpoudis; rule system and official examples)
- JEP 533: Structured Concurrency (Seventh Preview) (openjdk.org/jeps/533; Authors: Alan Bateman, Viktor Klang, Ron Pressler; API shape, the five changes, and official examples)
- openjdk/jdk source code (GitHub, tag jdk-27+35): markWord.hpp (L40–76, L116–154), globals.hpp (L131–132, L150), TransPatterns.java (L200–221, L510, L772–828, L935), StructuredTaskScope.java (L373–1468)
- JDK 27 project page JEP list (openjdk.org/projects/jdk/27, “JDK 27 reached General Availability on 15 September 2026”)