How a Tiny HashMap Bug Triggered a Massive Memory Leak in a High‑Traffic Microservice
A senior architect introduced a high‑concurrency monitoring feature that used a ConcurrentHashMap without proper equals/hashCode implementations, leading to duplicate keys, race conditions, and severe memory leaks, which were later resolved by correcting the key class and applying atomic map operations.
A new architect with extensive high‑concurrency experience joined the team and was tasked with the "highest concurrency" requirement: collecting the average response time and total request count for every API via an AOP interceptor.
The implementation stored monitoring data in a
ConcurrentHashMapwithout any comments, assuming the simple code needed no further documentation.
After deployment, the service began to suffer from memory overflow. Using Eclipse MAT and
jmap, the team discovered millions of
MonitorKeyand
MonitorValueobjects lingering in the heap.
<code>Monitor$MonitorKey@15aeb7ab</code>The root cause was that
MonitorKeydid not override
equalsand
hashCode, so identical keys were treated as distinct, causing unbounded growth of map entries.
In addition, the
visitmethod performed a non‑atomic sequence: retrieve the key, check for null, create a new value, and put it back. Concurrent threads could interleave, leading to lost updates:
<code>Thread1: get value for key a → null → create b → put a=b
Thread2: get value for key a → null → create c → put a=c</code>Two remediation approaches were discussed:
Adding
synchronizedto the method:
<code>public synchronized void visit(String url, String desc, long timeCost) { ... }</code>Using
putIfAbsentwith atomic counters:
<code>MonitorKey key = new MonitorKey(url, desc);
MonitorValue value = monitors.putIfAbsent(key, new MonitorValue());
value.count.getAndIncrement();
value.totalTime.getAndAdd(timeCost);
value.avgTime = value.totalTime.get() / value.count.get();</code>The technical director favored the synchronized version for its simplicity, while the team argued for the more efficient
putIfAbsentapproach.
After fixing the
equals/
hashCodeimplementation and applying a proper atomic update strategy, the memory leak disappeared and the service stabilized.
Key lessons: always override
hashCodeand
equalsfor objects used as map keys, ensure map operations are atomic in high‑concurrency scenarios, and prefer minimal, safe changes when fixing production bugs.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.