JDK-8366424 : Missing type profiling in generated Record Object methods
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang.invoke
  • Affected Version: 21,24
  • Priority: P4
  • Status: In Progress
  • Resolution: Unresolved
  • OS: generic
  • CPU: generic
  • Submitted: 2025-08-29
  • Updated: 2025-09-26
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 26
26Unresolved
Related Reports
Relates :  
Description
Java 16 record classes generate a hashCode() implementation that:

- Uses method handles to perform some reflection
- Invokes Objects::hash on record components to compute the hash code value

I've found this to perform significantly worse compared to hand-written (or Eclipse-generated) hashCode() implementations. A simple JMH benchmark illustrates the difference:

// ---------------------------------------------------
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class RecordHashCodeBenchmark {

    @State(Scope.Thread)
    public static class BenchmarkState {
        KeyClass k1 = new KeyClass(1, "a");
        KeyRecord k2 = new KeyRecord(1, "a");
    }

    @Benchmark
    public int classHashCode(BenchmarkState state) {
        return state.k1.hashCode();
    }

    @Benchmark
    public int recordHashCode(BenchmarkState state) {
        return state.k2.hashCode();
    }

    private static final record KeyRecord(Object key1, Object key2) {}

    private static final class KeyClass {
        private final Object key1;
        private final Object key2;

        KeyClass(Object key1, Object key2) {
            this.key1 = key1;
            this.key2 = key2;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((key1 == null) ? 0 : key1.hashCode());
            result = prime * result + ((key2 == null) ? 0 : key2.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            KeyClass other = (KeyClass) obj;
            if (key1 == null) {
                if (other.key1 != null)
                    return false;
            }
            else if (!key1.equals(other.key1))
                return false;
            if (key2 == null) {
                if (other.key2 != null)
                    return false;
            }
            else if (!key2.equals(other.key2))
                return false;
            return true;
        }

        @Override
        public String toString() {
            return "KeyClass [key1=" + key1 + ", key2=" + key2 + "]";
        }
    }
}
// ---------------------------------------------------


This performs as follows on my machine:

# JMH version: 1.37
# VM version: JDK 24.0.2, OpenJDK 64-Bit Server VM, 24.0.2+12
# VM invoker: C:\Program Files\Java\jdk-24.0.2+12\bin\java.exe
Benchmark                                Mode  Cnt           Score          Error  Units
RecordHashCodeBenchmark.classHashCode   thrpt    7  1115874313.008 ± 22812994.956  ops/s
RecordHashCodeBenchmark.recordHashCode  thrpt    7   282739491.955 ±  6590532.033  ops/s


Adding a third component to the record / class still produces a difference:


Benchmark                                Mode  Cnt          Score          Error  Units
RecordHashCodeBenchmark.classHashCode   thrpt    7  700717040.704 ± 15753956.255  ops/s
RecordHashCodeBenchmark.recordHashCode  thrpt    7  189052898.730 ±  4026557.417  ops/s


Perhaps the existing implementation relies on an assumption that escape analysis, inlining, and loop unrolling would kick in, making the two logically equivalent implementations perform the same? Apparently, this is the case for GraalVM JDK 24.0.2:
https://www.reddit.com/r/java/comments/1n1wafs/comment/nb6kjc6/


I'm not sure if this should be considered a problem in hotspot's JIT logic, or in javac's byte code generation for records.


Original discovery and motivation:
- https://github.com/jOOQ/jOOQ/issues/18935

A discussion and some further analyses:
- https://www.reddit.com/r/java/comments/1n1wafs/records_are_suboptimal_as_keys_in_hashmaps_or_as/
Comments
A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/27533 Date: 2025-09-26 22:13:00 +0000
26-09-2025

Consider this: public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((key1 == null) ? 0 : key1.hashCode()); result = prime * result + ((key2 == null) ? 0 : key2.hashCode()); return result; } public int hashCodeNoProfile() { final int prime = 31; int result = 1; result = prime * result + Objects.hashCode(key1); result = prime * result + Objects.hashCode(key2); return result; } Turns out hashCodeNoProfile is even slower than the record hashCode - so the cause is clearly JDK-8015417.
24-09-2025

Thanks for this report. Marking this as java.lang.invoke as this is responsibility of java.lang.runtime.
29-08-2025