Careeroza
Real World Example
Debugging an OutOfMemoryError in a Payments Service
A typical production story: a Spring Boot payments service in Kubernetes is restarted every few hours with OOMKilled. The team follows these steps.
- Add
-XX:+HeapDumpOnOutOfMemoryError -XX:MaxRAMPercentage=75 -Xlog:gc*:file=/logs/gc.log. - GC log shows old generation growing after every full GC: a leak, not just a small heap.
- Open the heap dump in Eclipse MAT → "Dominator tree": a static
HashMapholds 2 millionPaymentRequestobjects. - Root cause: a home-made cache with no eviction. Fix: replace it with a bounded cache (Caffeine with
maximumSizeand TTL).
The Leak and the Fix
Example
A bounded LRU cache using LinkedHashMap.removeEldestEntry keeps memory flat no matter how many requests arrive.
import java.util.*;
public class Main {
// BAD: grows forever, every entry stays reachable from a static field (a GC root)
static final Map<String, String> leakyCache = new HashMap<>();
// GOOD: bounded, least-recently-used entries are dropped
static final Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 1000;
}
};
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
String txnId = "TXN" + i;
leakyCache.put(txnId, "SUCCESS");
lruCache.put(txnId, "SUCCESS");
}
System.out.println("Leaky cache size: " + leakyCache.size());
System.out.println("LRU cache size: " + lruCache.size());
System.out.println("Oldest kept: " + lruCache.keySet().iterator().next());
}
}Output
Leaky cache size: 100000 LRU cache size: 1000 Oldest kept: TXN99000