How a “No‑Comment” Java Code Caused a Massive Memory Leak – Lessons on ConcurrentHashMap
A senior architect’s over‑confident, un‑commented Java code using ConcurrentHashMap triggered a severe memory leak, revealing pitfalls in hashCode/equals implementation, non‑atomic map updates, and the trade‑offs between synchronized blocks and putIfAbsent for high‑concurrency monitoring.
A new architect with a BAT background joined the department, boasting billions of high‑concurrency requests per second and a confidence that code need not be commented.
He was tasked with the highest‑concurrency requirement: collecting average response time and total request count for all interfaces, using an AOP‑wrapped ConcurrentHashMap where each request updates a shared key.
The initial implementation relied on a thread‑safe ConcurrentHashMap but omitted proper hashCode and equals overrides for the custom key objects, leading to massive memory growth as millions of distinct MonitorKey instances accumulated.
After the leak surfaced, the team used jmap and Eclipse MAT to analyze the heap, discovering that the map stored countless MonitorKey and MonitorValue objects without proper equality semantics. Monitor$MonitorKey@15aeb7ab The architect argued that the visit method did not need synchronization, but the code performed a non‑atomic check‑then‑act sequence:
Thread1: get value for key a
Thread2: get value for key a
Thread1: a is null, create b
Thread2: a is null, create c
Thread1: put a=b
Thread2: put a=cThis race caused lost updates and contributed to the memory issue.
Two solutions were proposed: adding synchronized to the method or using the atomic putIfAbsent operation. The synchronized approach was favored for its simplicity and minimal risk.
public synchronized void visit(String url, String desc, long timeCost) { ... }The alternative putIfAbsent implementation looked like:
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();After a heated discussion, the technical director insisted on the synchronized version to keep changes minimal and ensure production stability.
The incident was documented as a post‑mortem, emphasizing the importance of proper key implementations, atomic map operations, and cautious code reviews for high‑concurrency backend services.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
