News and views from members of the Java team at Oracle
JDK 27 continues the steady cadence of performance work across the JDK. More than 2,300 commits have landed in OpenJDK since JDK 26, including changes to the libraries, garbage collectors, JIT compiler, and runtime.
One change in particular will have especially broad reach: The "Compact Object Headers" feature is now enabled by default. Other broader changes include making G1 the default garbage collector everywhere.
Unless otherwise noted, the numbers below come from JMH benchmarks or workload measurements published with the corresponding pull requests. They show the local effect of a change, not its impact on an entire application; hardware, data shape, heap sizing, garbage collector, warmup, and compilation state all matter.
JEP 531 previews Lazy Constants for a third time. A LazyConstant<T> computes its value on first access and can be initialized only once. The JVM can trust that binding much like a final field, enabling optimizations such as constant folding without eager initialization or a manual double-checked-locking scheme.
JDK 27 removes the low-level isInitialized() and orElse() methods, whose use could undermine the intended initialization model, and adds Set.ofLazy(...) to the existing lazy list and map factories. As a preview API, Lazy Constants still require --enable-preview.
HashMap Bulk Copies
HashMap.putAll() and the HashMap(Map) constructor previously copied entries using the Map interface. That path creates an iterator and invokes methods such as next(), getKey(), getValue(), and hashCode(). When a call site sees several Map implementations, these calls can become megamorphic and difficult for the JIT compiler to optimize; an unmodifiable wrapper adds another layer of virtual calls.
JDK-8371656 (PR #28243) adds specialized paths when the source is exactly a HashMap or an unmodifiable map backed by one. The implementation walks the source table directly and reuses the hash stored in each node, avoiding iterator machinery and repeated hash-code calculation.
On AWS Graviton, with call sites deliberately exposed to five Map types, the reported operation-time reductions ranged from 61% for a five-entry HashMap to 86% for an unmodifiable 150-entry HashMap. In the latter case, the benchmark fell from about 10,593 ns/op to 1,533 ns/op. Other source-map types continue to use the general path.
Applications that perform text layout (for example in Swing GUIs) may spend surprising amounts of time in AttributedString. Its run attributes were represented by two arrays of Vector instances, so looking up an attribute for each character could require a linear Vector.indexOf() search.
JDK-8380794 (PR #30402) replaces that structure with one array of maps. In the submitted benchmark, iteration with one or more attributes took 35% to 40% less time. Creating a string with one attribute also allocated about 20% less memory; the difference was about 1% with four attributes and negligible with none. Code built on AttributedString, including text measurement and line-breaking APIs, can benefit.
URL.toString() delegates to URLStreamHandler.toExternalForm(), which also appears in startup and class-loading profiles. The old implementation built some components as intermediate concatenated strings and did not specialize for the common case in which optional components are absent.
JDK-8379557 (PR #30151) flattens the concatenation and adds fast paths for common URL shapes. Depending on which authority, query, and fragment components were present, the submitted benchmarks took at least 60% less time. Interpreter-only tests retained the improvement, showing that it does not depend solely on compiled execution.
On Unix, FileOutputStream and RandomAccessFile used to call fstat after opening a file for writing to reject directories. Because the relevant open operation already fails with EISDIR, the extra system call was redundant.
JDK-8373647 (PR #28823) removes that call. A stress benchmark fell from about 3,722 ns/op to 3,192 ns/op, a 14% reduction. The application-level gain depends on how often files are opened rather than reused, but the change removes a kernel transition from a common API path.
JDK 27 includes several architecture-specific and algorithmic improvements to cryptographic code:
JDK-8376164 (PR #29385) processes a complete AES/ECB message in an x86 intrinsic and adds round keys in parallel instead of entering a native stub once per 16-byte block. On an Intel Core i9-14900HX, the submitted benchmark improved from about 11.52 µs/op to 8.38 µs/op, or roughly 37% higher throughput.
JDK-8384353 (PR #31125) adds an AVX2 SHA-3 intrinsic and increases parallelism in the AVX-512 implementation. Submitted benchmarks improved SHA-3 message-digest performance by 16% to 39% with AVX2 and 24% to 33% with AVX-512. ML-DSA and ML-KEM operations built on SHA-3 also benefited. A follow-up, JDK-8386911 (PR #31648), removed unnecessary no-op Keccak calls and fixed regressions on configurations without the new intrinsic.
JDK-8383635 (PR #30991) improves the number-theoretic-transform multiplication used by ML-KEM. The submitted results show 9% to 17% improvements in encapsulation and decapsulation across the three key sizes.
The RISC-V port adds intrinsics for AES/CTR (JDK-8365732, PR #25243), AES/CBC (JDK-8371968, PR #28320), and GHASH (JDK-8373069, PR #28548). On AArch64, JDK-8371459 (PR #28515) selects a general-purpose-register SHA-3 intrinsic on processors where it is faster.
These gains are necessarily hardware-sensitive. HotSpot selects intrinsics according to the processor's reported capabilities, so evaluate them on the machines that will serve the workload.
JDK-8378698 (PR #29920) lets Base64.Encoder.encodeToString() use an internal trusted String constructor, avoiding an array copy.
Two changes close much of the performance gap between CharsetEncoder.canEncode(CharSequence) and canEncode(char) for common built-in charsets: JDK-8376226 (PR #29391) and JDK-8381015 (PR #30456).
JDK-8375580 (PR #29288) and JDK-8376403 (PR #29430) remove early ArrayDeque use from URLClassPath and ZipFile, respectively.
JDK-8376477 (PR #29442) avoids loading empty lock-helper classes used by Shutdown and ReferenceQueue.
JDK-8381710 (PR #31142) avoids lambdas in ModuleBootstrap during AOT training, reducing incidental class loading.
The library improvements above do not guarantee a better aggregate score. In our testing, minimal applications start in roughly the same time—or slightly more slowly—on JDK 27 than on JDK 26, while using up to 1 MB less memory.
HotSpot has selected G1 by default on server-class machines for many releases, while it could still select Serial GC when running in small or constrained environments. JEP 523 makes G1 the default everywhere in JDK 27.
This is primarily a consistency change, not a claim that G1 suits every workload. It is especially relevant to small containers, single-processor environments, and development machines that previously fell outside the server-class heuristic. Those deployments may see different pause times, CPU use, and memory overhead even when the application is unchanged. Serial GC remains available with -XX:+UseSerialGC.
A supporting change, JDK-8367993 (PR #28723), postpones most G1 concurrent-mark initialization until it is needed. Statistics buffers and concurrent-mark threads no longer have to be created during the earliest phase of VM startup. The submitted results show a small reduction in VM creation time—now relevant more broadly because G1 is the universal default.
Fixes for costly corner cases may not move typical benchmark scores, but they can be crucial in production:
JDK-8358342 (PR #31610) addresses poor scaling in G1's per-region code-root sets. Users had reported pauses longer than 90 seconds once the structure became expensive to scan and grow.
JDK-8377561 (PR #29673) fixes a Parallel GC feedback loop in which large allocations could trigger repeated Full GCs without enough heap expansion to make progress.
JDK-8379846 (PR #31189) makes G1's adaptive initiating-heap-occupancy calculation less sensitive to a single allocation-rate outlier, which could otherwise trigger endless concurrent cycles. JDK-8381006 (PR #30625) corrects the same calculation when periodic GCs are enabled.
Generational Shenandoah also continues to mature. JDK 27 improves old-generation evacuation (JDK-8376839, PR #29511); removes unnecessary per-object registration and card-alignment work from promotion buffers (JDK-8385606, PR #31316); and allows safepoint preemption while allocating very large arrays (JDK-8379531, PR #30567).
The Vector API remains incubating for a twelfth round, while both explicit Vector API code and ordinary loops benefit from work in the C2 compiler.
JDK-8342095 (PR #23413) teaches the SuperWord auto-vectorizer about subword casts. Loops that narrow or widen byte and short values can remain in vector form in more cases instead of falling back to scalar execution.
JDK-8370863 (PR #28313) removes redundant chains of vector-mask casts and recognizes load/cast/store round trips whose value is unchanged. In targeted JMH tests on an NVIDIA Grace system with 128-bit SVE2, throughput increased by approximately 2.3 to 3.8 times, depending on the element type and pattern.
Other vector work adds efficient unsigned minimum and maximum reductions on x86 (JDK-8346256, PR #29751) and AArch64 (JDK-8372980, PR #28693). JDK-8358521 (PR #25617) reassociates operations involving broadcast values, replacing some vector operations with a cheaper scalar calculation followed by one broadcast. Together, these changes let the JIT compiler turn more kinds of loops and Vector API operations into efficient SIMD instructions.
C2's KnownBits analysis now applies to more integer expressions. JDK-8367341 (PR #27618) uses known bits and unsigned bounds to simplify And and Or nodes, while JDK-8278857 (PR #29546) recognizes more shift-and patterns. Improvements to minimum and maximum identities (JDK-8373396, PR #28770; and JDK-8374896, PR #29517) likewise expose constants and redundant work for elimination.
Some work restores optimizations lost to earlier compiler changes. For example, JDK-8378713 (PR #30090) restores constant folding for Math.pow() cases that had regressed. Such fixes rarely make a release headline, but they prevent an upgrade from quietly slowing down code that was already well optimized.
Compact Object Headers were experimental in JDK 24 and became a product feature in JDK 25. JEP 534 completes the rollout by enabling them by default in JDK 27.
On a typical 64-bit HotSpot configuration, every object header shrinks from 12 bytes to 8 bytes. The saving is proportionally largest in heaps with many small objects. Better cache density and fewer bytes for the garbage collector to scan and copy can extend the benefit beyond footprint alone.
Measurements cited by JEP 519, which made the feature production-ready, included 22% lower heap usage and 8% lower CPU usage in one SPECjbb2015 configuration, 15% fewer collections with both G1 and Parallel GC in another configuration, and about 10% lower runtime in a highly parallel JSON parser benchmark. Results depend heavily on object-size distribution and alignment; an application dominated by large primitive arrays will benefit less than one dominated by small objects.
Compact headers can be disabled with -XX:-UseCompactObjectHeaders. This is useful for an A/B comparison during an upgrade and as a temporary fallback if a low-level tool or agent assumes the legacy object layout.
JDK-8379782 (PR #30331) enables the Object Monitor Table by default. The table keeps the mapping between Java objects and inflated monitors outside the object header. Disabling the table with -XX:-UseObjectMonitorTable restores the legacy representation, which embeds monitor pointers in object headers.
The new -XX:-UseObjectMonitorTable product flag was deprecated on introduction because it is transitional: the intent is to remove both the flag and the legacy implementation in a future release. Enabling the table by default exposed a 30% regression in a synchronization-heavy benchmark. JDK-8382311 (PR #30988) addressed it by reducing the number of fast-locking spin attempts before inflating a monitor under contention.
JDK-8373490 (PR #28781) eliminates pathological leak-profiler behavior that could cause long stalls when processing large object arrays.
JDK-8382740 (PR #30880) disables the jdk.OldObjectSample event with generational ZGC. Weakly referenced samples could otherwise survive until an old-generation collection and cause severe allocation stalls. This temporarily trades one profiling capability for more predictable application behavior.
Earlier entries in this series cover JDK 24, JDK 25, and JDK 26.
Sven Woltmann's Compact Object Headers in Java provides an illustrated explanation of the header layout and its memory effects.
Billy Korando's JDK 27 Runtime Updates Release Notes provides a short video (2 min) of major JDK 27 Features.
Mohamed Aboullaite's Java 27 features offers a broader overview of the release. The OpenJDK JDK 27 release page and the JEP, JBS, and pull-request links above remain the primary sources for exact status and implementation details.
The usual upgrade advice applies, with one addition: test the new defaults separately. Measure the application normally on JDK 27, then change one variable at a time—such as -XX:-UseCompactObjectHeaders or the selected garbage collector—to explain any material difference. Include startup time, allocation rate, live-set size, tail latency, and CPU consumption, not just peak throughput.
JDK 28 already has a healthy set of performance improvements integrated or in development, which we look forward to covering in spring 2027.
Until then, stay on the fast path!