Java Cup
Inside Java

News and views from members of the Java team at Oracle

Performance Improvements in JDK 27

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.

JDK Library Improvements

Lazy Constants Return for a Third Preview

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.

Faster 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.

Faster Attributed-Text Processing

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.

More Efficient URL Formatting

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.

Eliminated System Call When Opening Files

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.

Cryptographic Acceleration

JDK 27 includes several architecture-specific and algorithmic improvements to cryptographic code:

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.

More Library Improvements

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.

Garbage Collection

G1 Is Now the Default Everywhere

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.

Avoiding Pathological GC Behavior

Fixes for costly corner cases may not move typical benchmark scores, but they can be crucial in production:

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).

Compiler Improvements

More Vector Code Shapes Are Optimized

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.

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.

Better Scalar Simplification and Regression Recovery

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.

Runtime Improvements

Compact Object Headers Enabled by Default

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.

Object Monitor Table Enabled by Default

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.

JFR Improvements

Related Reading

Closing Thoughts

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!