Strings Just Got Faster
Per-Ake Minborg on May 1, 2025In JDK 25, we improved the performance of the class String
in such a way that the String::hashCode
function is mostly constant foldable. For example, if you use Strings as keys in a static unmodifiable Map
, you will likely see significant performance improvements.
Example
Here is a relatively advanced example where we maintain an immutable Map
of native calls, its keys are the name of the method call and the values are a MethodHandle
that can be used to invoke the associated system call:
// Set up an immutable Map of system calls
static final Map<String, MethodHandle> SYSTEM_CALLS = Map.of(
“malloc”, linker.downcallHandle(mallocSymbol,…),
“free”, linker.downcallHandle(freeSymbol…),
...);
…
// Allocate a memory region of 16 bytes
long address = SYSTEM_CALLS.get(“malloc”).invokeExact(16L);
…
// Free the memory region
SYSTEM_CALLS.get(“free”).invokeExact(address);
The method linker.downcallHandle(…)
takes a symbol and additional parameters to bind a native call to a Java MethodHandle
via the Foreign Function & Memory API introduced in JDK 22. This is a relatively slow process and involves spinning bytecode. However, once entered into the Map
, the new performance improvements in the String
class alone allow constant folding of both the key lookups and the values, thus improving performance by a factor of more than 8x:
--- JDK 24 ---
Benchmark Mode Cnt Score Error Units
StringHashCodeStatic.nonZero avgt 15 4.632 ± 0.042 ns/op
--- JDK 25 ---
Benchmark Mode Cnt Score Error Units
StringHashCodeStatic.nonZero avgt 15 0.571 ± 0.012 ns/op
Note : the benchmarks above are not using a malloc()
MethodHandle
but an int
identity function. After all, we are not testing the performance of malloc()
but the actual String
lookup and MethodHandle
performance.
This improvement will benefit any immutable Map<String, V>
with Strings as keys and where values (of arbitrary type V
) are looked up via constant Strings.
How Does It Work?
When a String
is first created, its hashcode is unknown. On the first call to String::hashCode
, the actual hashcode is computed and stored in a private field String.hash
. This transformation might sound odd; if String
is immutable, how can it mutate its state? The answer is that the mutation cannot be observed from the outside; String
would functionally behave the same regardless of whether or not an internal String.hash
cache field is used. The only difference is that it becomes faster for subsequent calls.
Now that we know how String::hashCode
works, we can unveil the performance changes made (which consists of a single line of code): the internal field String.hash
is marked with the JDK-internal @Stable
annotation. That’s it!
@Stable
tells the virtual machine it can read the field once and, if it is no longer its default value (zero), it can trust the field never change again. Hence, it can constant-fold the String::hashcode
operation and replace the call with the known hash
. As it turns out, the fields in the immutable Map
and the internals of the MethodHandle
are also trusted in the same way. This means the virtual machine can constant-fold the entire chain of operations:
- Computing the hash code of the String “malloc” (which is always
-1081483544
) - Probing the immutable
Map
(i.e., compute the internal array index which is always the same for themalloc
hashcode) - Retrieving the associated
MethodHandle
(which always resides on said computed index) - Resolving the actual native call (which is always the native
malloc()
call)
In effect, this means the native malloc()
method call can be invoked directly, which explains the tremendous performance improvements. To put it in other words, the chain of operation is completely short-circuited.
What Are the Ifs and Buts?
There is an unfortunate corner case that the new improvement does not cover: if the hash code of the String
happens to be zero, constant folding will not work. As we learned above, constant folding can only take place for non-default values (i.e., non-zero values for int
fields). However, we anticipate we will be able to fix this small impediment in the near future. You might think only one in about 4 billion distinct Strings has a hash code of zero and that might be right in the average case. However, one of the most common strings (the empty string “”) has a hash value of zero. On the other hand, no string with 1 - 6 characters (inclusive) (all characters ranging from ` ` (space) to Z
) has a hash code that is zero.
A Final Note
As @Stable
annotation is applicable only to internal JDK code, you cannot use it directly in your Java applications. However, we are working on a new JEP called JEP 502: Stable Values (Preview) that will provide constructs that allow user code to indirectly benefit from @Stable
fields in a similar way.
What’s the Next Step?
You can download JDK 25 already today and see how much this performance improvement will benefit your current applications,