Why Overriding hashCode Matters: Prevent Memory Leaks and OOM
This article explores the distinction between OutOfMemory errors and memory leaks in Java, examines how improper use of static fields, unclosed streams, incorrect equals/hashCode implementations, and ThreadLocal can cause leaks, and provides practical solutions and tools such as JVisualVM to detect and prevent these issues.
The discussion began with a group question: "What happens if you override equals but not hashCode?" This led to a deep dive into Java memory management, covering OutOfMemory (OOM) errors, memory leaks, and the subtle ways hashCode implementations can affect both.
Memory Overflow vs Memory Leak
OutOfMemory (OOM) simply means the JVM has run out of heap space. A memory leak occurs when objects that are no longer needed remain reachable, preventing the garbage collector from reclaiming them.
Memory leak: the heap contains objects that are no longer used, but the GC cannot delete them because they are still referenced.
When many such "leaked" objects accumulate, they eventually trigger an OOM.
Problems caused by memory leaks
Severe performance degradation during long‑running execution.
Unexpected application crashes.
Exhaustion of connection objects.
Ultimately, an OutOfMemory error.
Common scenarios causing memory leaks and solutions
Abuse of static member variables
Example:
@Slf4j
public class StaticTest {
public static List<Double> list = new ArrayList<>();
public void populateList() {
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
}
public static void main(String[] args) {
new StaticTest().populateList();
}
}The populateList() method can be called repeatedly, causing the static list to grow without bound.
Solution
Keep the collection local to the method so it is reclaimed after the stack frame finishes:
public void populateList() {
List<Double> list = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
}Static fields used by frameworks (e.g., Spring) are usually initialized once and do not grow with each call, so they are not a leak source.
Unclosed streams
Opening resources such as files, sockets, or database connections without closing them leaves useful objects reachable, preventing GC from reclaiming memory.
Solution
Close streams in a finally block.
Use Java 7+ try‑with‑resources which closes automatically.
Leverage Lombok's @Cleanup annotation.
@Cleanup InputStream jobJarInputStream = new URL(jobJarUrl).openStream();
@Cleanup OutputStream jobJarOutputStream = new FileOutputStream(jobJarFile);
IOUtils.copy(jobJarInputStream, jobJarOutputStream);Incorrect equals and hashCode implementation
When a class does not override hashCode, the default implementation (based on object identity) can cause duplicate logical objects to occupy separate map entries, effectively leaking memory.
public class MemLeakTest {
public static void main(String[] args) throws InterruptedException {
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("zhangsan", 1);
Person p2 = new Person("zhangsan", 1);
Person p3 = new Person("zhangsan", 1);
map.put(p1, "zhangsan");
map.put(p2, "zhangsan");
map.put(p3, "zhangsan");
System.out.println(map.entrySet().size()); // prints 3
}
}
@Getter @Setter
class Person {
private String name;
private Integer id;
public Person(String name, Integer id) { this.name = name; this.id = id; }
}Because each instance has a different identity hash, all three entries remain in the map.
Solution
Use Lombok's @Data (or manually generate) to provide consistent hashCode and equals based on business fields.
Another subtle case: modifying a field that participates in hashCode after the object is already in a HashSet makes removal fail, leading to apparent leaks.
Set<Person> set = new HashSet<>();
Person p3 = new Person("wanger", 3);
set.add(p3);
set.remove(p3); // fails after p3.setName(...)ThreadLocal misuse
ThreadLocal provides thread‑scoped variables but can retain references in thread pools after a request finishes, preventing GC of those objects.
Solution
Call ThreadLocal.remove() when the variable is no longer needed.
Wrap usage in a try/finally block and remove in the finally clause.
try {
threadLocal.set(System.nanoTime());
// business code
} finally {
threadLocal.remove();
}JVisualVM
JVisualVM (bundled with JDK 1.6+) visualizes heap, CPU, threads, and GC activity, helping to locate memory leaks. Install the VisualVM Launcher plugin in your IDE and launch the tool from the context menu.
Is hashCode generated from the object address?
OpenJDK defines six possible strategies for the default hashCode implementation. In JDK 1.8 the strategy is "5 – Xorshift combined with thread state", not a simple address‑based value.
0: Random number.
1: Function of the object's memory address.
2: Hard‑coded 1 (used for testing).
3: Sequence.
4: Memory address cast to int.
5: Thread state combined with Xorshift.
JDK 1.6 and 1.7 also use the Xorshift‑based approach, contrary to the common myth that hashCode is derived from the object address.
Object‑header hashCode and biased‑locking conflict
When Object.hashCode() or System.identityHashCode() is invoked, the JVM stores a hash value in the object header, which disables biased locking for that object. To keep biased locking effective, override hashCode with a custom implementation.
Conclusion
This article clarified the difference between OOM and memory leaks, presented common leak patterns (static fields, unclosed streams, faulty equals/hashCode, ThreadLocal), and offered concrete fixes and diagnostic tools. Understanding these low‑level details helps avoid subtle bugs that can crash Java applications.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
