Why Java’s hashCode Stays Consistent Even After GC Relocates Objects
This article explains why a Java object's hashCode remains unchanged after garbage‑collection moves it, detailing the hashCode contract, JVM header storage, OpenJDK generation strategies, identityHashCode behavior, and practical JOL experiments that illustrate the underlying mechanisms.
Introduction
When a Java object is moved by the HotSpot garbage collector, its memory address changes, yet calls to hashCode() must still return the same value. This article investigates how the JVM guarantees hashCode stability despite address relocation.
HashCode Contract
The java.lang.Object Javadoc defines three rules:
If the fields used by equals() do not change, repeated hashCode() calls must return the same integer.
Equal objects must have equal hash codes.
Unequal objects should preferably have different hash codes for performance, though collisions are allowed.
As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)
Experiment: Address vs. hashCode Before and After GC
Using the JOL library, the following Maven dependency is added:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>Sample program:
public class TestHashCode {
public static void main(String[] args) {
Object obj = new Object();
long address = VM.current().addressOf(obj);
long hashCode = obj.hashCode();
System.out.println("before GC : The memory address is " + address);
System.out.println("before GC : The hash code is " + hashCode);
new Object(); new Object(); new Object();
System.gc();
long afterAddress = VM.current().addressOf(obj);
long afterHashCode = obj.hashCode();
System.out.println("after GC : The memory address is " + afterAddress);
System.out.println("after GC : The hash code is " + afterHashCode);
System.out.println("memory address = " + (address == afterAddress));
System.out.println("hash code = " + (hashCode == afterHashCode));
}
}Running with a small heap (e.g., -Xms16m -Xmx16m -XX:+PrintGCDetails) produces output showing the address changes while the hash code stays identical.
Why hashCode Remains Unchanged
When hashCode() is first invoked, the JVM stores the computed value in a reserved part of the object header (25 bits on 32‑bit, 31 bits on 64‑bit). If the method is never called, that space remains zero, avoiding unnecessary overhead.
On subsequent calls, the JVM simply returns the stored value, so even after the object is moved by GC, the hash code does not need to be recomputed.
HashCode Generation Strategies in OpenJDK
OpenJDK defines six possible generation algorithms:
0 – Random number generator.
1 – Function of the object's memory address.
2 – Hard‑coded constant 1 (used for sensitivity testing).
3 – Sequence.
4 – Direct cast of the memory address to int.
5 – Combination of thread state and xorshift.
OpenJDK 6 and 7 use strategy 0 (random), while OpenJDK 8 and 9 default to strategy 5. Therefore, modern JVMs no longer rely on the object's address for hash code generation.
hashCode vs. identityHashCode
The default Object.hashCode() implementation delegates to System.identityHashCode(). When a subclass overrides hashCode(), the overridden method is used for that class, while System.identityHashCode() still returns the original, immutable value stored in the header.
Example:
public class Person {
private int id;
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Person p = new Person();
p.setId(1);
System.out.println("Hashcode = " + p.hashCode());
System.out.println("Identity Hashcode = " + System.identityHashCode(p));Output shows the overridden hash code (e.g., 32) differs from the identity hash code (e.g., 1259475182), confirming the two mechanisms are independent.
Additional Verification with JOL
Using JOL to print the object layout before and after invoking hashCode() reveals that the first word of the header changes from its initial pattern to the stored hash value, confirming the lazy‑storage behavior.
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println(obj.hashCode());
System.out.println(ClassLayout.parseInstance(obj).toPrintable());The printed header shows the value at offset 0 changing from 01 00 00 00 to a non‑zero integer representing the stored hash code.
Conclusion
The apparent conflict between mutable object addresses and immutable hash codes is resolved by the JVM’s lazy header storage and by modern hash code generation algorithms that no longer depend on the address. Understanding these mechanisms also clarifies the relationship between hashCode() and System.identityHashCode(), and demonstrates how JOL can be used to inspect object internals.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Senior Brother's Insights
A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
